React render multiple layouts with react-router-dom v6
Step-by-step guide to render multiple layouts in React using the new react-router-dom
version 6.
Lots of things has changed in the new react-router-dom
package such as:
component
replaced withelement
exact
prop is no longer supportedSwitch
replaced withRoutes
useHistory()
replaced withuseNavigate()
Redirect
replaced withNavigate
Okay, let’s see how things will be.
1 — Create new application
Open your terminal and CD to where you want to create your project:
$ npx create-react-app multiple-layouts
In case you want to create your app with typescript:
$ npx create-react-app multiple-layouts --template typescript
2 — Install react router dom and lodash
Now let’s install react-router-dom
package, and also we’re going to use the lodash
package later in our coding:
$ npm install react-router-dom lodash
// Or
$ yarn add react-router-dom lodash
By the time this article was written, the exact version is: 6.6.2
3 — Create pages
Let’s create some pages in order to start setting our logic:
Create a new /pages
folder inside the /src
folder and create 4 pages:
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— Create layouts
Now, let’s create our layouts, considering we have only 2 layouts:
AnonymousLayout
— when users are not logged into the app.MainLayout
— when users logged into the app.
Inside /src
folder, create a /layouts
folder that will contain both layouts:
const AnonymousLayout = () => {
return (
<div>AnonymousLayout</div>
)
}
export default AnonymousLayout;
const MainLayout = () => {
return (
<div>MainLayout</div>
)
}
export default MainLayout;
5 — Create routes files
Okay cool!! Now, let’s head to create our routes.
As the previous examples, inside the /src
folder create a new folder with the name /routes
that will contain these files:
/routes/index.js
— the list of pages and layouts./routes/ProtectedRoute/index.jsx
— the component that will protect our routes and prevent non-logged users from accessing pages./routes/generate-routes.jsx
— in this file we’re going to loop through our routes and generate routes and layouts.
First of all, let’s create our routes array.
Note: Routes array can be used also to render navigation items in the header or sidebar. So, we consider having navigation links in a sidebar along side with sub-items or sub-menus.
/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'
}
]
}
]
}
];
Let’s take a closer look for each property and see how it can be useful:
— Layouts:
- layout: The target layout that will wrap the target page. [Required]
- routes: The list of routes will be rendered inside the layout. [Required]
— Layout routes:
- name: The name of the route, this must be unique as it will be used as a key when mapping routes. [Required]
- title: The text that will be displayed as browser tab title and navigation link label. [Required]
- hasSiderLink: A boolean prop to indicate if the target route should be rendered as a sidebar navigation link or not. When set to true, the route will appear inside the sidebar. [Optional]
- component: The page component to be rendered inside the layout when paths has been matched. [Optional]
- path: The associated path for the page component. [Optional]
- isPublic: A boolean prop to indicate if the page is public or requires a login. When set to true, the page will be accessible with the anonymous mode. [Optional]
- routes: The list of sub-routes for a specific route. When rendering sub routes as drop-down navigation links, the parent route should not have a path or component. [Optional]
Note: You can customize the props to fit the requirement of your project.
/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;
In the code above, we’ve created a ProtectedRoute
component to secure require-login pages from non-logged users.
Considering that we are authorized, the component will accept to props:
- isPiblic: A boolean prop to tell if the current route should be protected or not.
- isAuthorized: A boolean prop to tell if the user has a valid JWT or not.
If the isPublic
or the isAuthorized
is true, the component will return an Outlet
component.
An
<Outlet>
should be used in parent route elements to render their child route elements — reactrouter docs.
The rest of the explanation will come later… Read the full docs about <Outlet>
here.
/routes/generate-routes.jsx
So, let’s import the:
Route
,Routes as ReactRoutes
fromreact-router-dom
ProtectedRoute
fromProtectedRoute
flattenDeep
fromlodash/flattenDeep
Note: You can name ReactRoutes
whatever you want, in this example the generateFlattenRoutes
function will return a component named Routes
.
import flattenDeep from 'lodash/flattenDeep';
import React from 'react';
import { Route, Routes as ReactRoutes } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
Then, let’s create a function that takes our routes and flatten them in the same level. Hard to understand ? No problem, let’s take the example below:
// 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.
I hope the above example explained the case.
Continue in our generateFlattenRoutes
function:
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)]));
}
The is a recursive function that calls itself each time a route has nested routes array. If the function receives an undefined param, it will return an empty array to prevent the app from crashing. As we know, the property routes is optional.
Next, we’re creating the main function will generate our 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;
}
The function above will take the list of layouts as param, And return a component named Routes
that takes a single prop named isAuthorized
and it will be passed later as a prop to the ProtectedRoute
component.
The renderRoutes
function will return the component Routes
once it done generating routes.
Now, the component Routes
alse will return list of mapped 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 isPublic={isPublic} 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;
}
Let’s take a look of what the piece of code above does:
1 — Create a const named layouts
what will have the result of mapping the mainRoutes
param.
2 — In the map function’s callback, we’re extracting the layout
and its list of routes
. Of course we’ve renamed the layout
to Layout
in order to use it later as React Component. Also, we’ve considered the item index
in our callback function.
3 — Inside the callback function, the const subRoutes
will contain our flat routes by invoking the generateFlattenRoutes
function and passing the extracted routes as an argument.
const subRoutes = generateFlattenRoutes(routes);
Now, as a result… we’re returning an HTML element:
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 — The Route
element is a component imported from react-router-dom
. The key prop will take the index of map iteration, and the element (component in the v.5
of react-router-dom
) will take the <Layout />
. This Route
will be rendered as <Layout />
.
The child element for this route is also a Route
element, but this will represent the ProtectedRoute
component, and the element prop will take the <ProtectedRoute />
component with its props.
Inside the ProtectedRoute
component, we’ll generate children as we’re using the v.5
of react-router-dom
except, the component
prop is now named element
as mentioned before.
5 — The subRoutes
const is our flat routes, so we’re calling the method map to generate the routes will be displayed as children in the ProtectedRoute
component.
Now, back to the <Outlet>
component returned in the ProtectedRoute
component. It will tell the react-router-dom
where to render children.
We’ve made the same thing with Layout
component.
return (
<Route key={index} element={<Layout />}>
{/* ... */}
</Route>
)
6 — Update layouts content
As a result, we should update both layouts to return an <Outlet>
to tell where to render children… like the following:
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 — Invoke routes generator function
Now that our routes generator is done, lets go back to the /routes/index.js
file and invoke the above function and export the result like following:
// 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 — implement routing system
Now that we prepared our routing system, the final step begins.
In the index.jsx
file, wrap the App
component with the element BrowserRouter
imported from 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>
);
Then, moving to the App.jsx
file, import Routes
from ./routes
like below:
import React, { useEffect } from 'react';
import { Routes } from './routes';
const App = () => {
return (
<Routes isAuthorized={true} />
);
}
export default App;
9— Conclusion
Now we’ve seen how to deal with multiple layouts in the 6th version of react-router-dom. Also, we’ve seen how to create dynamic routes instead of having a single file with all the routes added one-by-one.
Thank you for reading this post and I hope it was helpful. And if you have any questions, please let me know in comment section. I’ll happily answer your questions. And cheer me if you like the post.
Happy coding guys ❤
Big thanks to: Sami Bouafif