React Router, версия 6. Часть 2 из 4
21.12.2021
Теги: Frontend • JavaScript • React.js • Web-разработка • Маршрутизация • Модуль
Блог
Давайте вместо простого компонента Blog.js
создадим в директории src/blog
несколько других — BlogLayout.js
, BlogIndex.js
, BlogCategory.js
и BlogArticle.js
. Кроме того, создадим файл с данными BlogData.js
, где будем хранить категории и статьи блога. Но для начала изменим компонент App.js
.
import { Routes, Route } from 'react-router-dom' import AppLayout from './pages/AppLayout.js' import Home from './pages/Home.js' import About from './pages/About.js' import NotFound from './pages/NotFound.js' import BlogLayout from './blog/BlogLayout.js' import BlogIndex from './blog/BlogIndex.js' import BlogCategory from './blog/BlogCategory.js' import BlogArticle from './blog/BlogArticle.js' function App() { return ( <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<Home />} /> <Route path="blog" element={<BlogLayout />}> <Route index element={<BlogIndex />} /> <Route path="category/:id" element={<BlogCategory />} /> <Route path="article/:id" element={<BlogArticle />} /> </Route> <Route path="about" element={<About />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ) } export default App
Теперь у нас два уровня вложенности Route
— кроме того, что есть вложенные по отношению к path="/"
маршруты blog
и about
, есть еще вложенные по отношению к path="blog"
маршруты category
и article
.
import { Link, Outlet } from 'react-router-dom' import { categories } from './BlogData.js' const BlogLayout = () => { return ( <> <h1>Our blog</h1> <ul> {categories.map(category => <li key={category.id}> <Link to={`category/${category.id}`}>{category.title}</Link> </li> )} </ul> <Outlet /> </> ) } export default BlogLayout
import { Link } from 'react-router-dom' import { articles } from './BlogData.js' const BlogIndex = () => { if (articles.length === 0) { return <p>Статей еще нет</p> } return ( <> <h2>Все статьи блога</h2> <ul> {articles.map(item => <li key={item.id}> <Link to={`article/${item.id}`}>{item.title}</Link> </li> )} </ul> </> ) } export default BlogIndex
import { useParams, Link } from 'react-router-dom' import NotFound from '../pages/NotFound.js' import { categories, articles } from './BlogData.js' const BlogCategory = () => { const {id} = useParams() const category = categories.find(item => item.id == id) const categoryArticles = articles.filter(item => item.category == id) return category ? ( <> <h2>Категория: {category.title}</h2> {categoryArticles.length ? ( <ul> {categoryArticles.map(item => <li key={item.id}> <Link to={`../article/${item.id}`}>{item.title}</Link> </li> )} </ul> ) : ( <p>Нет статей в этой категории</p> )} </> ) : ( <NotFound /> ) } export default BlogCategory
import { useParams } from 'react-router-dom' import { articles } from './BlogData.js' import NotFound from '../pages/NotFound.js' const BlogArticle = () => { const {id} = useParams() const article = articles.find(article => article.id == id) return article ? <h2>{article.title}</h2> : <NotFound /> } export default BlogArticle
export const categories = [ {id: 1, title: 'JavaScript'}, {id: 2, title: 'React.js'}, {id: 3, title: 'Node.js'}, ] export const articles = [ {id: 1, title: 'Первая статья о JavaScript', category: 1}, {id: 2, title: 'Вторая статья о JavaScript', category: 1}, {id: 3, title: 'Первая статья о React.js', category: 2}, {id: 4, title: 'Вторая статья о React.js', category: 2}, {id: 5, title: 'Первая статья о Node.js', category: 3}, {id: 6, title: 'Вторая статья о Node.js', category: 3}, ]
Здесь надо обратить внимание, как задаются ссылки для Link
— они относительные. Ссылки на категории в компоненте BlogLayout
указаны относительно blog
, а ссылки на статьи в компоненте BlogCategory
— относительно blog/category
.
Исходные коды приложения здесь, директория 2.
Custom Link
Вместо использования NavLink
мы можем создать свой компонент CustomLink
на основе компонента Link
, для этого создаем директорию components
и внутри нее — файл CustomLink.js
.
import { Link, useMatch } from 'react-router-dom'; const CustomLink = (props) => { const {children, to, ...others} = props const match = useMatch(to) const style = { color: match ? 'var(--color-active)' : 'white' } return ( <Link to={to} style={style} {...others}> {children} </Link> ) } export default CustomLink
Документация говорит, что useMatch
возвращает true
(на самом деле объект), если переданный аргумент совпадает с текущим URL (то есть тем, который сейчас в адресной строке браузера). Аргумент может быть просто строкой или объектом типа {path, caseSensitive, end}
. Теперь в App.js
вместо NavLink
используем CustomLink
.
import { Outlet } from 'react-router-dom' import CustomLink from '../components/CustomLink.js' const AppLayout = () => { return ( <> <header> <CustomLink to="/">Home</CustomLink> <CustomLink to="/blog">Blog</CustomLink> <CustomLink to="/about">About</CustomLink> </header> <main className="container"> <Outlet /> </main> <footer className="container">Copyright 2021</footer> </> ) } export default AppLayout
Использование строки в качестве аргумента работает, но не слишком хорошо — для блога ссылка подсвечивается, но только для /blog
. Подсветка не работает для /blog/category/:id
и /blog/article/:id
— потому что работает строгое совпадение to
с текущим URL. Давайте доработаем:
import { Link, useMatch, useResolvedPath } from 'react-router-dom'; const CustomLink = (props) => { const { children, to, ...others } = props const resolved = useResolvedPath(to) const match = useMatch({path: resolved.pathname, end: to === '/' ? true : false}) const style = { color: match ? 'var(--color-active)' : 'white' } return ( <Link to={to} style={style} {...others}> {children} </Link> ) } export default CustomLink
Параметры
При разработке блога мы уже использовали параметры /blog/category/:id
и /blog/category/:id
. Доступ к параметрам можно получить через хук useParams
— это мы тоже знаем. Но давайте для полноты картины рассмотрим еще один пример. Пусть у нас на сайте есть раздел «Услуги», который содержит информацию об услугах, которые предоставляет компания. Создадим файл с данными services/ServiceData.js
, где перечислим все услуги.
const services = [ {slug: 'first', title: 'First service'}, {slug: 'second', title: 'Second service'}, {slug: 'third', title: 'Third service'} ] export default services
Изменим компонент App.js
:
import { Routes, Route } from 'react-router-dom' import AppLayout from './pages/AppLayout.js' import Home from './pages/Home.js' import About from './pages/About.js' import NotFound from './pages/NotFound.js' import BlogLayout from './blog/BlogLayout.js' import BlogIndex from './blog/BlogIndex.js' import BlogCategory from './blog/BlogCategory.js' import BlogArticle from './blog/BlogArticle.js' import Services from './services/Services.js' import Service from './services/Service.js' function App() { return ( <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<Home />} /> <Route path="services" element={<Services />}> <Route path=":slug" element={<Service />} /> </Route> <Route path="blog" element={<BlogLayout />}> <Route index element={<BlogIndex />} /> <Route path="category/:id" element={<BlogCategory />} /> <Route path="article/:id" element={<BlogArticle />} /> </Route> <Route path="about" element={<About />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ) } export default App
Добавим ссылку на раздел «Услуги» в pages/Layout.js
:
import { Outlet } from 'react-router-dom' import CustomLink from '../components/CustomLink.js' const AppLayout = () => { return ( <> <header> <CustomLink to="/">Home</CustomLink> <CustomLink to="/blog">Blog</CustomLink> <CustomLink to="/services">Services</CustomLink> <CustomLink to="/about">About</CustomLink> </header> <main className="container"> <Outlet /> </main> <footer>Copyright 2021</footer> </> ) } export default AppLayout
Создадим компоненты services/Services.js
и services/Service.js
:
import { Link, Outlet } from 'react-router-dom' import services from './ServiceData.js' const Services = () => { return ( <> <h1>Services</h1> <ul> {services.map(item => <li key={item.slug}> <Link to={item.slug}>{item.title}</Link> </li> )} </ul> <Outlet /> </> ) } export default Services
import { useParams } from 'react-router-dom' import NotFound from '../pages/NotFound.js' import services from './ServiceData.js' const Service = () => { const {slug} = useParams() const service = services.find(item => item.slug === slug) return service ? <h2>{service.title}</h2> : <NotFound /> } export default Service
Исходные коды приложения здесь, директория 3.
Поиск: Frontend • JavaScript • React.js • Web-разработка • Маршрутизация • Модуль