React.js. Маршрутизация. Часть третья из трех
25.08.2021
Теги: Frontend • JavaScript • React.js • Web-разработка • Маршрутизация • Навигация • Теория
Давайте еще доработаем наше приложение и добавим маленький блог. Категорий у блога не будет, а постов будет всего два — но для нас этого достаточно. Создадим директорию 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> <NavLink to="/catalog" activeClassName="active">Catalog</NavLink> <NavLink to="/blog" activeClassName="active">Blog</NavLink> <NavLink to="/about" activeClassName="active">About</NavLink> <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