React.js. Маршрутизация. Часть третья из трех

25.08.2021

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

Давайте еще доработаем наше приложение и добавим маленький блог. Категорий у блога не будет, а постов будет всего два — но для нас этого достаточно. Создадим директорию src/blog и разместим в ней два компонента и контекст (в котором будем хранить посты блога). И посмотрим, как передавать параметры в маршрутах и потом получать к ним доступ.

import {createContext} from 'react';

const BlogContext = createContext();

const BlogContextProvider = (props) => {
    const posts = [
        {id: 111, title: 'Post one', content: 'Content of post one'},
        {id: 222, title: 'Post two', content: 'Content of post two'},
    ];
    const value = {
        posts: posts
    };
    return (
        <BlogContext.Provider value={value}>
            {props.children}
        </BlogContext.Provider>
    );
}

export {BlogContext, BlogContextProvider};
import {useContext} from 'react';
import {BlogContext} from './BlogContext';
import {Link, Switch, Route} from 'react-router-dom';
import ShowPost from './ShowPost';

export default function ShowList() {
    const blog = useContext(BlogContext);
    return (
        <div>
            <h1>Blog</h1>
            {blog.posts.length ? (
                <div>
                    <ul>
                    {blog.posts.map(item => (
                        <li key={item.id}>
                            <Link to={'/blog/post/' + item.id}>{item.title}</Link>
                        </li>
                    ))}
                    </ul>
                    <Switch>
                        <Route path="/blog/post/:id([0-9]+)" component={ShowPost} />
                    </Switch>
                </div>
            ) : (
                <p>No posts</p>
            )}
        </div>
    )
}
import {useContext} from 'react';
import {BlogContext} from './BlogContext';

export default function ShowPost(props) {
    // получаем досуп к контексту блога
    const blog = useContext(BlogContext);
    // получаем идентификатор поста блога
    const id = parseInt(props.match.params.id);
    // находим пост по идентификатору
    const post = blog.posts.find(item => item.id === id);
    return (post ? (
            <div>
                <h3>{post.title} (#{post.id})</h3>
                <p>{post.content}</p>
            </div>
        ) : (
            <p>Post not found</p>
        )
    )
}

Добавим ссылку на каталог в компонент NavBar:

import {NavLink} from 'react-router-dom';

export default function NavBar() {
    return (
        <div>
            <NavLink exact to="/" activeClassName="active">Home</NavLink>&nbsp;
            <NavLink to="/catalog" activeClassName="active">Catalog</NavLink>&nbsp;
            <NavLink to="/blog" activeClassName="active">Blog</NavLink>&nbsp;
            <NavLink to="/about" activeClassName="active">About</NavLink>&nbsp;
            <NavLink to="/contact" activeClassName="active">Contact</NavLink>
        </div>
    )
}

Добавим в компонент Content еще один Route:

import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import Home from '../pages/Home';
import About from '../pages/About';
import Contact from '../pages/Contact';
import NotFound from '../pages/NotFound';
import NavBar from './NavBar';
import Catalog from '../catalog/Catalog';
import ShowList from '../blog/ShowList';

export default function Content() {
    return (
        <main className="container">
            <Router>
                <NavBar />
                <Switch>
                    <Route exact path="/" component={Home} />
                    <Route path="/catalog" component={Catalog} />
                    <Route path="/blog" component={ShowList} />
                    <Route exact strict path="/about" component={About} />
                    <Route exact strict path="/contact" component={Contact} />
                    <Route component={NotFound} />
                </Switch>
            </Router>
        </main>
    );
}

Для возможности доступа к контексту изменим компонент App:

import React from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import Content from './components/Content';
import {BlogContextProvider} from './blog/BlogContext';

export default function App() {
    return (
        <React.Fragment>
            <Header />
            <BlogContextProvider>
                <Content />
            </BlogContextProvider>
            <Footer />
        </React.Fragment>
    );
}

Когда мы используем атрибут componenet={ShowPost} в <Route> — у компонента ShowPost в объекте props будут три свойства — match, location и history. Давайте на них посмотрим подробнее:

import {useContext} from 'react';
import {BlogContext} from './BlogContext';

export default function ShowPost(props) {
    console.log('props.macth=', props.match);
    console.log('props.location=', props.location);
    console.log('props.history=', props.history);

    const blog = useContext(BlogContext);
    const id = parseInt(props.match.params.id);
    const post = blog.posts.find(item => item.id === id);
    return (post ? (
            <div>
                <h3>{post.title} (#{post.id})</h3>
                <p>{post.content}</p>
            </div>
        ) : (
            <p>Post not found</p>
        )
    )
}
/* props.match */
{
    isExact: true
    params: {id: "111"}
    path: "/blog/post/:id([0-9]+)"
    url: "/blog/post/111"
    [[Prototype]]: Object
}
/* props.location */
{
    hash: ""
    key: "1wc5kn"
    pathname: "/blog/post/111"
    search: ""
    state: undefined
    [[Prototype]]: Object
}
/* props.history */
{
    action: "POP"
    block: ƒ block(prompt)
    createHref: ƒ createHref(location)
    go: ƒ go(n)
    goBack: ƒ goBack()
    goForward: ƒ goForward()
    length: 31
    listen: ƒ listen(listener)
    location: {
        pathname: "/blog/post/111",
        search: "",
        hash: "",
        state: undefined,
        key: "1wc5kn"
    }
    push: ƒ push(path, state)
    replace: ƒ replace(path, state)
    [[Prototype]]: Object
}

Попробуем использовать функцию props.history.goBack():

export default function ShowPost(props) {
    /* .......... */
    return (post ? (
            <div>
                <h3>{post.title} (#{post.id})</h3>
                <p>{post.content}</p>
                <button className="btn" onClick={() => props.history.goBack()}>Back</button>
            </div>
        ) : (
            <p>Post not found</p>
        )
    )
}

Использование хуков

В связи с последними веяниями в React-разработке, стало популярным использование хуков. Разработчики Router-а не отстают от мейнстрима — есть возможность использования хуков useHistory, useLocation, useParams и useRouteMatch. Давайте изменим ShowPost, чтобы использовать хуки useHistory и useParams.

import {useContext} from 'react';
import {BlogContext} from './BlogContext';
import {useParams, useHistory} from 'react-router';

export default function ShowPost(props) {
    const blog = useContext(BlogContext);
    const history = useHistory();
    const params = useParams();
    const id = parseInt(params.id);
    const post = blog.posts.find(item => item.id === id);
    return (post ? (
            <div>
                <h3>{post.title} (#{post.id})</h3>
                <p>{post.content}</p>
                <button className="btn" onClick={() => history.goBack()}>Back</button>
            </div>
        ) : (
            <p>Post not found</p>
        )
    )
}

Хук useRouteMatch пытается найти совпадение для текущего URL, действуя аналогично <Route>. Например, если мы его разместим в компоненте ShowList — сможем увидеть, какая часть текущего URL совпала с атрибутом path вышестоящего <Route>.

export default function ShowList() {
    const blog = useContext(BlogContext);
    const match = useRouteMatch();
    console.log(match);
    /* .......... */
}
/* http://localhost:3000/blog */
{
    isExact: true
    params: {}
    path: "/blog"
    url: "/blog"
    [[Prototype]]: Object
}
/* http://localhost:3000/blog/post/111 */
{
    isExact: false
    params: {}
    path: "/blog"
    url: "/blog"
    [[Prototype]]: Object
}

Вышестоящий <Route> расположен в компоненте Content:

<Switch>
    ..........
    <Route path="/blog" component={ShowList} />
    ..........
</Switch>

Когда запрошена главная страница блога — есть точное совпадение (isExact:true) текущего URL с атрибутом path вышестоящего <Route>. Когда запрошена страница отдельного поста блога — совпадение будет не точным (isExact:false), потому что совпадает только /blog.

Мы можем изменить ShowList таким образом, чтобы при точном совпадении показывать только список постов (и на этом закончить). А при неточном совпадении — попытаться найти совпадение URL с /blog/post/:id, чтобы показать отдельный пост блога.

import {useContext} from 'react';
import {BlogContext} from './BlogContext';
import {Link, Switch, Route, useRouteMatch} from 'react-router-dom';
import ShowPost from './ShowPost';

export default function ShowList() {
    const blog = useContext(BlogContext);
    const match = useRouteMatch();
    if (blog.posts.length === 0) { // в блоге еще нет постов
        return <p>No posts</p>
    }
    if (match.isExact) { // запрошена страница списка постов
        return (
            <ul>
            {blog.posts.map(item => (
                <li key={item.id}>
                    <Link to={'/blog/post/' + item.id}>{item.title}</Link>
                </li>
            ))}
            </ul>
        );
    }
    return ( // запрошена страница отдельного поста блога
        <Switch>
            <Route path="/blog/post/:id([0-9]+)" component={ShowPost} />
        </Switch>
    )
}

Вообще, match.url и match.path удобно использовать для создания вложенных <Link> и вложенных <Route>.

<Switch>
    ..........
    <Route path="/blog">
        <ShowList />
    </Route>
    ..........
</Switch>
export default function ShowList() {
    const match = useRouteMatch();
    return (
        <>
            <nav>
                <Link to={`${match.url}/post/111`}>Post one</Link>
                <Link to={`${match.url}/post/222`}>Post two</Link>
            </nav>
            <Switch>
                <Route path={`${match.path}/post/:id`}>
                    <ShowPost />
                </Route>
            </Switch>
        </>
    );
}
export default function ShowPost() {
    const params = useParams();
    return <h3>#{params.id}</h3>;
}

Дополнительно

Поиск: JavaScript • React.js • Web-разработка • Frontend • Маршрутизация • Навигация • Теория • Route • Router • Switch • Link • NavLink • Redirect

Каталог оборудования
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.