React Router, версия 6. Часть 2 из 4

21.12.2021

Теги: FrontendJavaScriptReact.jsWeb-разработкаМаршрутизацияМодуль

Блог

Давайте вместо простого компонента 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-разработка • Маршрутизация • Модуль

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.