React render multiple layouts with react-router-dom v6

Najm Eddine Zaga
9 min readJan 31, 2023

--

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 with element
  • exact prop is no longer supported
  • Switch replaced with Routes
  • useHistory() replaced with useNavigate()
  • Redirect replaced with Navigate

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 from react-router-dom
  • ProtectedRoute from ProtectedRoute
  • flattenDeep from lodash/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

--

--

Najm Eddine Zaga

Software Developer & Javascript enthusiastic | TypeScript | ReactJs | NextJs | NestJs | ExpressJs | Redux