Рендеринг нескольких макетов в React с помощью react-router-dom v6.
Пошаговое руководство по рендерингу нескольких макетов в React с использованием новой версии react-router-dom 6.
Многое изменилось в новом пакете react-router-dom, такие как:
component
заменен наelement
- свойство
exact
больше не поддерживается Switch
заменен наRoutes
useHistory()
заменен наuseNavigate()
Redirect
заменен наNavigate
Хорошо, давайте посмотрим, как все будет.
1 - Создать новое приложение.
Откройте терминал и перейдите в папку, где вы хотите создать свой проект:
$ npx create-react-app multiple-layouts
В случае, если вы хотите создать свое приложение с использованием TypeScript:
$ npx create-react-app multiple-layouts --template typescript
2 - Установите react-router-dom и lodash.
Теперь давайте установим пакет react-router-dom
, а также мы будем использовать пакет lodash
в нашем коде.
$ npm install react-router-dom lodash
// Or
$ yarn add react-router-dom lodash
К моменту написания этой статьи точная версия составляет 6.10.0.
3 - Создание страниц
Давайте создадим несколько страниц, чтобы начать настраивать нашу логику работы с роутингом.
Создайте новую папку /pages внутри папки /src и создайте 4 страницы:
-
src/pages/Login/index.jsx
const Login = () => { return ( <div>Login</div> ) } export default Login;
-
src/pages/Home/index.jsx
const Home = () => { return ( <div>Home</div> ) } export default Home;
-
src/pages/ListUsers/index.jsx
const CreateUser = () => { return ( <div>CreateUser</div> ) } export default CreateUser;
-
src/pages/CreateUser/index.jsx
const ListUsers = () => { return ( <div>ListUsers</div> ) } export default ListUsers;
4 - Создание макетов
Давайте создадим наши макеты, учитывая, что у нас всего 2 варианта макета:
- AnonymousLayout — используется, когда пользователи не вошли в приложение.
- MainLayout — используется, когда пользователи вошли в приложение.
Внутри папки /src создайте папку /layouts, которая будет содержать оба макета.
AnonymousLayout:
const AnonymousLayout = () => {
return (
<div>AnonymousLayout</div>
)
}
export default AnonymousLayout;
MainLayout:
const MainLayout = () => {
return (
<div>MainLayout</div>
)
}
export default MainLayout;
5 - Создание файлов маршрутов
Отлично! Теперь давайте перейдем к созданию наших маршрутов.
Как и в предыдущих примерах, внутри папки /src создайте новую папку с именем /routes, которая будет содержать следующие файлы:
/routes/index.js
— это список страниц и макетов./routes/ProtectedRoute/index.jsx
— это компонент, который будет защищать наши маршруты и предотвращать доступ неавторизованных пользователей к страницам./routes/generate-routes.jsx
— в этом файле мы будем перебирать наши маршруты и генерировать маршруты и макеты.
Прежде всего, давайте создадим наш массив маршрутов.
Примечание: Массив маршрутов можно использовать также для отображения элементов навигации в заголовке или боковой панели. Поэтому мы рассматриваем наличие ссылок навигации в боковой панели вместе с подэлементами или подменю.
/routes/index.js
// Layouts
import AnonymousLayout from "../layouts/AnonymousLayout";
import MainLayout from "../layouts/MainLayout";
// Pages
import Login from "../pages/Login";
import Home from "../pages/Home";
import CreateUser from "../pages/CreateUser";
import ListUsers from "../pages/ListUsers";
export const routes = [
{
layout: AnonymousLayout,
routes: [
{
name: 'login',
title: 'Login page',
component: Login,
path: '/login',
isPublic: true,
}
]
},
{
layout: MainLayout,
routes: [
{
name: 'home',
title: 'Home page',
component: Home,
path: '/home'
},
{
name: 'users',
title: 'Users',
hasSiderLink: true,
routes: [
{
name: 'list-users',
title: 'List of users',
hasSiderLink: true,
component: ListUsers,
path: '/users'
},
{
name: 'create-user',
title: 'Add user',
hasSiderLink: true,
component: CreateUser,
path: '/users/new'
}
]
}
]
}
];
Давайте более детально рассмотрим каждое свойство и посмотрим, как оно может быть полезным:
Макеты:
- layout: Целевой макет, который будет оборачивать целевую страницу. [Обязательно]
- routes: Список маршрутов, которые будут отображаться внутри макета. [Обязательно]
Маршруты макета:
- name: Название маршрута, которое должно быть уникальным, так как оно будет использоваться в качестве ключа при отображении маршрутов. [Обязательно]
- title: Текст, который будет отображаться в качестве заголовка вкладки браузера и метки навигации. [Обязательно]
- hasSiderLink: Булевое свойство, указывающее, должен ли целевой маршрут отображаться в качестве ссылки навигации боковой панели или нет. Когда установлено значение true, маршрут будет отображаться внутри боковой панели. [Необязательно]
- component: Компонент страницы, который будет отображаться внутри макета, когда будут сопоставлены пути. [Необязательно]
- path: Связанный путь для компонента страницы. [Необязательно]
- isPublic: Булевое свойство, указывающее, является ли страница общедоступной или требует входа в систему. Когда установлено значение true, страница будет доступна в анонимном режиме. [Необязательно]
- routes: Список подмаршрутов для конкретного маршрута. При отображении подмаршрутов в виде выпадающих ссылок навигации, у родительского маршрута не должно быть пути или компонента. [Необязательно]
/routes/ProtectedRoute/index.jsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
const ProtectedRoute = ({ isPublic, isAuthorized }) => {
return (isPublic || isAuthorized) ? <Outlet /> : <Navigate to='/login' />
}
export default ProtectedRoute;
В приведенном выше коде мы создали компонент ProtectedRoute
для защиты страниц, требующих входа в систему, от неавторизованных пользователей.
Учитывая, что мы авторизованы, компонент будет принимать два свойства:
- isPublic: логическое свойство, которое указывает, должен ли быть защищен текущий маршрут или нет.
- isAuthorized: логическое свойство, которое указывает, имеет ли пользователь действительный JWT или нет.
Если isPublic
или isAuthorized
равно true
, компонент вернет компонент Outlet
.
Компонент <Outlet> должен использоваться в родительских элементах маршрута для отображения дочерних элементов маршрута - документация react-router.
Остальное объяснение будет дано позже... Читайте полную документацию о компоненте <Outlet> здесь.
/routes/generate-routes.jsx
Итак, давайте импортируем:
- Route, Routes как ReactRoutes из react-router-dom.
- ProtectedRoute из ProtectedRoute.
- flattenDeep из lodash/flattenDeep.
Примечание: Вы можете назвать ReactRoutes
, как угодно, в этом примере функция generateFlattenRoutes
вернет компонент с именем Routes
.
import flattenDeep from 'lodash/flattenDeep';
import React from 'react';
import { Route, Routes as ReactRoutes } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
Затем давайте создадим функцию, которая берет наши маршруты и выравнивает их на одном уровне. Трудно понять? Нет проблем, давайте рассмотрим пример ниже:
// The function will take this array.
[2, 4, [5, 41, [100, 200], 500], 10, [50, 30], 30];
// And take all values out to the same level.
[2, 4, 5, 41, 100, 200, 500, 10, 50, 30, 30]
// There will be no nested arrays at all.
Я надеюсь, что приведенный выше пример объяснил суть.
Продолжим в нашей функции generateFlattenRoutes:
import flattenDeep from 'lodash/flattenDeep';
import React from 'react';
import { Route, Routes as ReactRoutes } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
const generateFlattenRoutes = (routes) => {
if (!routes) return [];
return flattenDeep(routes.map(({ routes: subRoutes, ...rest }) => [rest, generateFlattenRoutes(subRoutes)]));
}
Это рекурсивная функция, которая вызывает саму себя каждый раз, когда у маршрута есть вложенный массив маршрутов. Если функция получает неопределенный параметр, она вернет пустой массив, чтобы предотвратить сбой приложения. Как мы знаем, свойство routes является необязательным.
Затем мы создаем основную функцию, которая будет генерировать наши маршруты:
import flattenDeep from 'lodash/flattenDeep';
import React from 'react';
import { Route, Routes as ReactRoutes } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
const generateFlattenRoutes = (routes) => {
if (!routes) return [];
return flattenDeep(routes.map(({ routes: subRoutes, ...rest }) => [rest, generateFlattenRoutes(subRoutes)]));
}
export const renderRoutes = (mainRoutes) => {
const Routes = ({ isAuthorized }) => {
// code here
}
return Routes;
}
Функция выше принимает список макетов в качестве параметра и возвращает компонент с именем Routes
, который принимает единственное свойство с именем isAuthorized
, которое будет передано позже в качестве свойства компоненту ProtectedRoute
.
Функция renderRoutes
вернет компонент Routes
, когда закончит генерацию маршрутов.
Компонент Routes
также вернет список отображенных маршрутов.
import flattenDeep from 'lodash/flattenDeep';
import React from 'react';
import { Route, Routes as ReactRoutes } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
const generateFlattenRoutes = (routes) => {
if (!routes) return [];
return flattenDeep(routes.map(({ routes: subRoutes, ...rest }) => [rest, generateFlattenRoutes(subRoutes)]));
}
export const renderRoutes = (mainRoutes) => {
const Routes = ({ isAuthorized }) => {
const layouts = mainRoutes.map(({ layout: Layout, routes }, index) => {
const subRoutes = generateFlattenRoutes(routes);
return (
<Route key={index} element={<Layout />}>
<Route element={<ProtectedRoute isAuthorized={isAuthorized} />}>
{subRoutes.map(({ component: Component, path, name }) => {
return (
Component && path && (<Route key={name} element={<Component />} path={path} />)
)
})}
</Route>
</Route>
)
});
return <ReactRoutes>{layouts}</ReactRoutes>;
}
return Routes;
}
Давайте посмотрим, что делает этот кусок кода:
- Создается константа с именем
layouts
, которая будет содержать результат отображения параметраmainRoutes
. - В обратном вызове функции отображения мы извлекаем макет и его список маршрутов. Конечно, мы переименовали макет в
Layout
, чтобы использовать его позже в качестве компонента React. Также мы учитываем индекс элемента в нашей функции обратного вызова. - Внутри функции обратного вызова константа
subRoutes
будет содержать наши плоские маршруты, вызывая функциюgenerateFlattenRoutes
и передавая извлеченные маршруты в качестве аргумента.
const subRoutes = generateFlattenRoutes(routes);
Теперь, в результате... мы возвращаем элемент HTML:
return (
<Route key={index} element={<Layout />}>
<Route element={<ProtectedRoute isAuthorized={isAuthorized} />}>
{subRoutes.map(({ component: Component, path, name }) => {
return (
Component && path && (<Route key={name} element={<Component />} path={path} />)
)
})}
</Route>
</Route>
)
-
Элемент
Route
- это компонент, импортированный изreact-router-dom
. Свойствоkey
примет индекс итерации отображения, а элемент (компонент в v.5 react-router-dom) примет <Layout />. Этот Route будет отображаться как <Layout />.Дочерний элемент для этого маршрута также является элементом
Route
, но он будет представлять компонентProtectedRoute
, а свойство элемента примет компонент <ProtectedRoute /> со своими свойствами.Внутри компонента
ProtectedRoute
мы будем генерировать дочерние элементы, используя v.5react-router-dom
, за исключением того, что свойство компонента теперь называется element, как упоминалось ранее. -
Константа
subRoutes
- это наши плоские маршруты, поэтому мы вызываем метод map, чтобы сгенерировать маршруты, которые будут отображаться в качестве дочерних элементов в компонентеProtectedRoute
.Теперь вернемся к компоненту <Outlet>, возвращаемому в компоненте
ProtectedRoute
. Он сообщит react-router-dom, где отображать дочерние элементы.Мы сделали то же самое с компонентом
Layout
.
return (
<Route key={index} element={<Layout />}>
{/* ... */}
</Route>
)
6 - Обновление содержимого макетов.
В результате мы должны обновить оба макета, чтобы они возвращали <Outlet>, указывающий, где рендерить дочерние элементы... как показано ниже:
AnonymousLayout
import React from 'react';
import { Outlet } from 'react-router-dom';
const AnonymousLayout = () => {
return (
<Outlet />
)
}
export default AnonymousLayout;
MainLayout
import React from 'react';
import { Outlet } from 'react-router-dom';
const MainLayout = () => {
return (
<Outlet />
)
}
export default MainLayout;
7 - Вызов функции генерации маршрутов.
Теперь, когда наш генератор маршрутов готов, вернемся к файлу /routes/index.js
и вызовем вышеуказанную функцию, экспортируя результат следующим образом:
// Layouts
import AnonymousLayout from "../layouts/AnonymousLayout";
import MainLayout from "../layouts/MainLayout";
// Pages
import Login from "../pages/Login";
import Home from "../pages/Home";
import CreateUser from "../pages/CreateUser";
import ListUsers from "../pages/ListUsers";
// Don't mess with this code
export const routes = [
{...},
{...}
]
// Just add this line
export const Routes = renderRoutes(routes);
8 - Реализация системы маршрутизации
Теперь, когда мы подготовили нашу систему маршрутизации, начинается последний шаг.
В файле index.jsx
оберните компонент App элементом BrowserRouter
, импортированным из react-router-dom
:
import React from 'react';
import ReactDOM from 'react-dom/client';;
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Затем, переходя к файлу App.jsx
, импортируйте Routes
из ./routes
следующим образом:
import React, { useEffect } from 'react';
import { Routes } from './routes';
const App = () => {
return (
<Routes isAuthorized={true} />
);
}
export default App;
9 - Заключение
Теперь мы увидели, как работать с несколькими макетами в 6-й версии react-router-dom. Кроме того, мы увидели, как создавать динамические маршруты вместо того, чтобы иметь один файл со всеми маршрутами, добавленными по одному.