React Router, версия 6. Часть 3 из 4
22.12.2021
Теги: Frontend • JavaScript • React.js • Web-разработка • Маршрутизация • Модуль
useNavigate
Хук useHistory
был удален из React Router 6, теперь вместо него useNavigate
. В принципе, использование этих двух хуков мало чем отличается. Хук возвращает функцию, которая в качестве первого аргумента принимает строку URL или целое число. Целое число может быть положительным (движение вперед по истории браузера) или отрицательным (движение назад по истории браузера). В качестве второго аргумента можно передать объект {replace, state}
.
const navigate = useNavigate() const goBack = () => navigate(-1) const goHome = () => navigate('/home')
Работа с историей браузера допускает две операции — push
(добавить в историю) и replace
(заместить в истории). По умолчанию при вызове navigate()
происходит push
, но при желании можно это изменить на replace
. Допустим, страница на сайте по адресу /old-page
устарела, вместо нее мы создали новую /new-page
. Но в интернете остались ссылки на старую страницу, посетители продолжают на нее переходить. И нам надо всех перенаправлять на новую страницу. Но в истории эту старую страницу сохранять не нужно — иначе пользователь не сможет вернуться туда, откуда пришел на наш сайт. Каждый раз при нажатии кнопки «Назад» он будет попадать на /old-page
, а оттуда — опять на /new-page
.
Давайте создадим компоненты OldPage.js
и NewPage.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' import Services from './services/Services.js' import Service from './services/Service.js' import OldPage from './pages/OldPage.js' import NewPage from './pages/NewPage.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="old-page" element={<OldPage />} /> <Route path="new-page" element={<NewPage />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ) } export default App
import { useNavigate } from 'react-router-dom' import { useEffect } from 'react' const OldPage = () => { const navigate = useNavigate() useEffect(() => { navigate('/new-page', {replace: true, state: {from: 'old-page'}}) }, []) return <h1>Old page</h1> } export default OldPage
import { useLocation } from 'react-router-dom' const NewPage = () => { const { state } = useLocation() return ( <> <h1>New page</h1> {state?.from && <p>From {state.from}</p>} </> ) } export default NewPage
Обратите внимание, что если установить replace
в false
, то попав на страницу New Page
после клика на Old Page
— уйти с этой страницы с помощью кнопки «Назад» браузера уже не получится. Переход назад по истории приводит на Old Page
, а оттуда сразу на New Page
.
Navigate
Компонент позволяет перенаправить пользователя на новую страницу. То, что мы делали с помощью useNavigate
можно сделать гораздо проще с помощью Navigate
. По сути, это обертка для useNavigate
и принимает пропсы to
, replace
и state
.
import { Routes, Route, Navigate } 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' import OldPage from './pages/OldPage.js' import NewPage from './pages/NewPage.js' const Redirect = <Navigate to="/new-page" replace={true} state={{from: 'old-page'}} /> 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="old-page" element={Redirect} /> <Route path="new-page" element={<NewPage />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ) } export default App
const OldPage = () => { return <h1>Old page</h1> } export default OldPage
import { useLocation } from 'react-router-dom' const NewPage = () => { const { state } = useLocation() return ( <> <h1>New page</h1> {state?.from && <p>From {state.from}</p>} </> ) } export default NewPage
Защищенные маршруты
Допустим, у нас есть страница, которая должна быть доступна только после авторизации пользователя. Чтобы это обеспечить, создадим в директории auth
HOC-компонент RequireAuth.js
. Этот компонент должен оборачивать компонент той страницы, которая должна быть доступна только авторизованным пользователям.
const Admin = () => { return <h1>Admin page</h1> } export default Admin
import { useLocation, Navigate } from 'react-router-dom' const RequireAuth = (props) => { const auth = true const location = useLocation() if (!auth) { return <Navigate to="/login" state={{from: location}} /> } return props.children } export default RequireAuth
Если пользователь авторизован, то он получит доступ к Admin page
. А если не авторизован — будет отправлен на страницу ввода пароля.
const AdminAuthRequire = ( <RequireAuth> <Admin /> </RequireAuth> ) function App() { return ( <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<Home />} /> <Route path="blog" element={<Blog />} /> <Route path="about" element={<About />} /> <Route path="admin" element={AdminAuthRequire} /> <Route path="login" element={<Login />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ) }
Теперь нам нужен контекст AuthContext.js
, где мы будем хранить состояние (авторизован или нет) + две функции для входа и выхода.
import { createContext, useState } from 'react' export const AuthContext = createContext() export const AuthProvider = (props) => { const [auth, setAuth] = useState(false) const login = (password, success, failure) => { if (password === 'qwerty') { setAuth(true) success() } else { setAuth(false) failure() } } const logout = () => { setAuth(false) } const value = {auth, login, logout} return ( <AuthContext.Provider value={value}> {props.children} </AuthContext.Provider> ) }
Чтобы контекст был доступен везде, отредактируем App.js
:
function App() { return ( <AuthProvider> <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<Home />} /> <Route path="blog" element={<Blog />} /> <Route path="about" element={<About />} /> <Route path="admin" element={AdminAuthRequire} /> <Route path="login" element={<Login />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> </AuthProvider> ) }
Для удобства получения контекста создадим хук useAuthContext.js
:
import { useContext } from 'react'; import { AuthContext } from './AuthContext.js'; export default function useAuthContext() { const authContext = useContext(AuthContext) return authContext }
И создадим компонент Login.js
, где можно ввести пароль:
import { useState } from 'react'; import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import useAuthContext from './useAuthContext.js' const Login = () => { const { auth, login } = useAuthContext() const navigate = useNavigate() const location = useLocation() const [invalid, setInvalid] = useState(false) // если пользователь авторизован, ему здесь делать нечего if (auth) { return <Navigate to="/admin" replace={true} /> } // откуда был перенаправлен пользователь, чтобы вернуть // его обратно после правильного ввода пароля const fromPage = location.state?.from?.pathname || '/' const handleSubmit = (event) => { event.preventDefault() const form = event.target const password = form.password.value // функции, которые будут выполнены в случае правильного // и неправильного ввода пароля для авторизации const success = () => navigate(fromPage, {replace: true}) const failure = () => setInvalid(true) login(password, success, failure) } return ( <div> <h1>Login page</h1> <form onSubmit={handleSubmit}> <label> Password: <input name="password" /> </label> <button type="submit">Login</button> </form> {invalid && <p style={{color:'red'}}>Неверный пароль</p>} <p>From page: {fromPage}</p> </div> ) } export default Login
Почти все готово, осталось только доработать RequireAuth.js
, чтобы использовать контекст:
import { useLocation, Navigate } from 'react-router-dom' import useAuthContext from './useAuthContext.js' const RequireAuth = (props) => { const location = useLocation() const { auth } = useAuthContext() if (!auth) { return <Navigate to="/login" state={{from: location}} /> } return props.children } export default RequireAuth
Для полноты картины — исходный код компонента App.js
:
import { Routes, Route, Navigate } 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' import OldPage from './pages/OldPage.js' import NewPage from './pages/NewPage.js' import Login from './auth/Login.js' import Admin from './auth/Admin.js' import Edit from './auth/Edit.js' import RequireAuth from './auth/RequireAuth.js' import { AuthProvider } from './auth/AuthContext.js' const Redirect = <Navigate to="/new-page" replace={true} state={{from: 'old-page'}} /> const AdminAuthRequire = ( <RequireAuth> <Admin /> </RequireAuth> ) const EditAuthRequire = ( <RequireAuth> <Edit /> </RequireAuth> ) function App() { return ( <AuthProvider> <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="old-page" element={Redirect} /> <Route path="new-page" element={<NewPage />} /> <Route path="admin" element={AdminAuthRequire} /> <Route path="edit" element={EditAuthRequire} /> <Route path="login" element={<Login />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> </AuthProvider> ) } export default App
Исходные коды приложения здесь, директория 4.
Поиск: Frontend • JavaScript • React.js • Web-разработка • Маршрутизация • Модуль