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

22.12.2021

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

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-разработка • Маршрутизация • Модуль

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