Пошаговое руководство по рендерингу нескольких макетов в React с использованием новой react-router-dom версии 6.

В новом пакете react-router-dom многое изменилось, например:

  • component заменено на element
  • exact реквизит больше не поддерживается
  • Switch заменено на Routes
  • useHistory() заменено на useNavigate()
  • Redirect заменено на Navigate

Ладно, посмотрим, как обстоят дела.

1 — Создать новое приложение

Откройте свой терминал и компакт-диск, где вы хотите создать свой проект:

$ npx create-react-app multiple-layouts

Если вы хотите создать свое приложение с машинописным текстом:

$ 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.6.2

3 — Создание страниц

Давайте создадим несколько страниц, чтобы начать настройку нашей логики:

Создайте новую папку /pages внутри папки /src и создайте 4 страницы:

  • src/pages/Login/index.jsx
  • src/pages/Home/index.jsx
  • src/pages/ListUsers/index.jsx
  • src/pages/CreateUser/index.jsx
const Login = () => {
  return (
    <div>Login</div>
  )
}

export default Login;
const Home = () => {
  return (
    <div>Home</div>
  )
}

export default Home;
const CreateUser = () => {
  return (
    <div>CreateUser</div>
  )
}

export default CreateUser;
const ListUsers = () => {
  return (
    <div>ListUsers</div>
  )
}

export default ListUsers;

4— Создание макетов

Теперь давайте создадим наши макеты, учитывая, что у нас всего 2 макета:

  • AnonymousLayout — когда пользователи не вошли в приложение.
  • MainLayout — когда пользователи вошли в приложение.

Внутри папки /src создайте папку /layouts, которая будет содержать оба макета:

const AnonymousLayout = () => {
  return (
    <div>AnonymousLayout</div>
  )
}

export default AnonymousLayout;
const MainLayout = () => {
  return (
    <div>MainLayout</div>
  )
}

export default MainLayout;

5 — Создать файлы маршрутов

ОК, круто!! Теперь давайте начнем создавать наши маршруты.

Как и в предыдущих примерах, внутри папки /src создайте новую папку с именем /routes, которая будет содержать следующие файлы:

  • /routes/index.js  —  список страниц и макетов.
  • /routes/ProtectedRoute/index.jsx — компонент, который защитит наши маршруты и предотвратит доступ к страницам незарегистрированных пользователей.
  • /routes/generate-routes.jsx— в этом файле мы будем перебирать наши маршруты и генерировать маршруты и макеты.

Прежде всего, давайте создадим наш массив маршрутов.

Примечание. Массив Routes также можно использовать для отображения элементов навигации в заголовке или на боковой панели. Поэтому мы рассматриваем возможность размещения навигационных ссылок на боковой панели рядом с подэлементами или подменю.

/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'
          }
        ]
      }
    ]
  }
];

Давайте подробнее рассмотрим каждое свойство и посмотрим, чем оно может быть полезно:

— Макеты:

  • макет. Целевой макет, в который будет помещена целевая страница. [Обязательно]
  • маршруты. Список маршрутов будет отображаться внутри макета. [Обязательно]

— Схема маршрутов:

  • name: имя маршрута, оно должно быть уникальным, так как оно будет использоваться в качестве ключа при сопоставлении маршрутов. [Обязательно]
  • title. Текст, который будет отображаться как заголовок вкладки браузера и метка навигационной ссылки. [Обязательно]
  • hasSiderLink: логическое свойство, указывающее, должен ли целевой маршрут отображаться как ссылка для навигации на боковой панели или нет. Если установлено значение true, маршрут появится внутри боковой панели. [Необязательно]
  • component: компонент страницы, который будет отображаться внутри макета при совпадении путей. [Необязательно]
  • путь связанный путь для компонента страницы. [Необязательно]
  • isPublic: логическое значение, указывающее, является ли страница общедоступной или требует входа. Если установлено значение true, страница будет доступна в анонимном режиме. [Необязательно]
  • маршруты. Список подмаршрутов для определенного маршрута. При отображении подмаршрутов в виде раскрывающихся навигационных ссылок родительский маршрут не должен иметь пути или компонента. [Необязательный]

Примечание. Вы можете настроить реквизит в соответствии с требованиями вашего проекта.

/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 для защиты страниц с требованием входа в систему от незарегистрированных пользователей.

Учитывая, что мы авторизованы, компонент примет в props:

  • isPiblic: логическое значение, указывающее, должен ли текущий маршрут быть защищен или нет.
  • isAuthorized: логическое значение, указывающее, есть ли у пользователя действительный JWT или нет.

Если isPublic или isAuthorized истинно, компонент вернет компонент Outlet.

<Outlet> следует использовать в родительских элементах маршрута для отображения их дочерних элементов маршрута — документы reactrouter.

Остальные объяснения будут позже… Полную документацию по <Outlet> читайте здесь.

/routes/generate-routes.jsx

Итак, давайте импортируем:

  • Route, Routes as 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)]));
}

Это рекурсивная функция, которая вызывает себя каждый раз, когда маршрут имеет вложенный массив маршрутов. Если функция получает неопределенный параметр, она вернет пустой массив, чтобы предотвратить сбой приложения. Как мы знаем, свойство route является необязательным.

Далее мы создаем основную функцию, которая будет генерировать наши маршруты:

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;
}

Давайте посмотрим, что делает приведенный выше фрагмент кода:

1 — Создайте константу с именем layouts, которая будет иметь результат сопоставления параметра mainRoutes.

2 — В обратном вызове функции карты мы извлекаем layout и его список routes. Конечно, мы переименовали layout в Layout, чтобы использовать его позже как компонент React. Кроме того, мы рассмотрели элемент index в нашей функции обратного вызова.

3 — Внутри функции обратного вызова константа 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>
)

4 — Элемент Route является компонентом, импортированным из react-router-dom. Key prop будет принимать индекс итерации карты, а элемент (компонент в v.5 из react-router-dom) будет принимать <Layout /> . Этот Route будет отображаться как <Layout /> .

Дочерний элемент для этого маршрута также является элементом Route, но он будет представлять компонент ProtectedRoute, а свойство element будет принимать компонент <ProtectedRoute /> со своими реквизитами.

Внутри компонента ProtectedRoute мы будем генерировать дочерние элементы, поскольку мы используем v.5 из react-router-dom, за исключением того, что свойство component теперь называется element, как упоминалось ранее.

5 — Константа subRoutes — это наши плоские маршруты, поэтому мы вызываем карту метода для генерации маршрутов, которые будут отображаться как дочерние элементы в компоненте ProtectedRoute.

Теперь вернемся к компоненту <Outlet>, возвращенному в компоненте ProtectedRoute. Он скажет react-router-dom, где отображать дочерние элементы.

То же самое мы сделали с компонентом Layout.

return (
  <Route key={index} element={<Layout />}>
    {/* ... */}
  </Route>
)

6 — Обновить содержимое макетов

В результате мы должны обновить оба макета, чтобы они возвращали <Outlet>, чтобы указать, где отображать дочерние элементы… как показано ниже:

import React from 'react';
import { Outlet } from 'react-router-dom';

const AnonymousLayout = () => {
  return (
    <Outlet />
  )
}

export default AnonymousLayout;
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. Кроме того, мы увидели, как создавать динамические маршруты вместо одного файла со всеми маршрутами, добавляемыми один за другим.

Спасибо, что прочитали этот пост, и я надеюсь, что он был полезен. И если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать в разделе комментариев. Я с радостью отвечу на ваши вопросы. И поддержите меня, если вам понравился пост.

Удачного кодирования, ребята ❤

Большое спасибо: Сами Буафиф