18 Jul 2023 · Software Engineering

    How to Build a Routing Layer in React and Why You Need It

    12 min read
    Contents

    In a single-page React Application, routing refers to the process of navigating between different pages without triggering a full page reload. The application initially loads a single HTML page. Then, it dynamically renders different components based on user interaction.

    Since React does not have built-in routing capabilities, you need to implement it using a third-party library. Each library is different, but what usually happens is that you end up spreading the routing logic throughout your React application.

    Now, imagine adding a layer to your architecture that encapsulates what is required to route between pages in React. Having the routing logic in one place would bring several benefits to your architecture and make your codebase more maintainable. Not bad, eh?

    Let’s now understand what a routing layer is and learn how to implement it in React.

    What is a routing layer?

    A routing layer is the part of a frontend React application responsible for managing paths and rendering the appropriate page components. When a user clicks on a link or enters a URL in the address bar, the routing layer intercepts the request and determines which component or view should be rendered based on the current path.

    In particular, a basic routing layer in React consists of a basic react-router-dom setup and the following two elements:

    • PathConstants: An object that contains all page routes.
    const PathConstants = {
        TEAM: "/team",
        REPORT_ANALYSIS: "reports/:reportId/analysis",
        // ...
    }
    • routes: An array that includes the mappings between a route string and the page components.
    const routes = [
        { path: PathConstants.TEAM, element: <TeamPage /> },
        { path: PathConstants.REPORT_ANALYSIS, element: <ReportAnalysisPage /> },
        // ...
    ]

    To register a new route, you only have to add a new constant to PathConstants and associate it with a page component in routes. Also, all links to that page should use the string path defined in PathConstants:

    <a href={PathConstants.TEAM}>Go to the team page!</a>

    In other words, you just centralized most of the routing logic of your React app in just a couple of files. This is what a routing layer is all about!

    Let’s now dig into the benefits that such an approach to routing brings to your React architecture.

    Why your react app should have a routing layer

    There are many reasons why you should adopt a routing layer in a frontend architecture. In detail, it allows you to:

    • Centralize routing logic: in a routing layer, all route mapping logic is centralized in a few files. Specifically, this means that you can place all the custom loaders, actions, fetchers introduced in react-router-dom v6.4 in it.
    • Enhance performance: instead of loading all page components at the beginning, you can configure React to lazily load them only when required. This mechanism is called code-splitting and leads to better performance when a user navigates through a React app. To enable it, you need to use lazy imports on page components. With a routing layer, all page imports are in the same file. Thus, controlling the code-splitting feature becomes easier.
    • Improve maintainability: thanks to a routing layer, you can specify all the route mappings in the same place. Now, if a page needs to change its route, you only have to update one constant defined in the routing layer. Without this, you would have to look for all route string references and manually update them.

    These are all good reasons why your React app should have a routing layer. Keep reading and see how to add it to your frontend React architecture.

    Implement a routing layer in a React app with React Router DOM

    Follow this step-by-step tutorial and learn how to build a routing layer in React.

    Prerequisites

    Handling routing in an SPA application in React becomes easier with the right library. Here, you will look at how to implement a routing layer with react-router-dom, the most popular library that provides client-side routing capabilities to React apps. Keep in mind that you can achieve a similar result with any other routing library.

    You can add react-router-dom to your project’s dependencies with the command below:

    npm install react-router-dom

    To follow this guide, you will need version 6.4 or higher. If your project relies on an older version of the library, you can follow the official migration guide to upgrade it.

    Setting up client routing with React Router DOM

    First, you need to add a router to your React app. The most popular one offered by React Router DOM is a browser router, which relies on the DOM History API to manage the page URL and update the history stack. Create one with createBrowserRouter()function by updating App.js, as follows:

    // ./App.js
    
    import {
      createBrowserRouter,
      RouterProvider,
    } from "react-router-dom"
    import Home from "./pages/Home"
    // ...
    import About from "./pages/About"
    
    function App() {
      // initialize a browser router
      const router = createBrowserRouter([
        {
          path: "/",
          element: <Home />,
        },
        // other pages....
        {
          path: "/about",
          element: <About />,
        },
      ])
    
      return (
          <RouterProvider router={router} />
      )
    }
    
    export default App

    This is what a basic setup of React Router DOM looks like. When you now visit the "/" and "/about" pages, react-router-dom will load the <Home> and <About> components, respectively. In general, whenever you click on a <Link> component, React Router will mount a specific page as defined in the router object.

    In a real-world scenario, your web app in React is likely to follow a layout. For example, it might have a header, footer, and/or side menu. Right now, react-router-dom re-renders the entire page when you click on a link. Ideally, it should only reload the content area of the template.

    To achieve that, you can define a Layout component as follows:

    // ./src/components/Layout.js
    
    import { Outlet } from "react-router-dom"
    import Header from "./Header"
    import Footer from "./Footer"
    
    export default function Layout() {
        return (
            <>
                <Header />
                <main>                
                    <Outlet />
                </main>
                <Footer />
            </>
        )
    }

    Do not forget that this is just an example of a custom <Layout> component. Yours might look different. What you need to focus your attention on is the special <Outlet> component from react-router-dom. This allows a parent route element to render its child route elements. In other words, it will be replaced by child page components.

    You can update App.js to render child routes in <Layout>, as shown below:

    // ./App.js
    
    import {
      createBrowserRouter,
      RouterProvider,
    } from "react-router-dom"
    import Layout from "./components/page"
    import Home from "./pages/Home"
    // ...
    import About from "./pages/About"
    
    function App() {
      const router = createBrowserRouter([
        {
          // parent route component
          element: <Layout />,
          // child route components
          children: [
            {
              path: "/",
              element: <Home />,
            },
            // other pages....
            {
              path: "/about",
              element: <About />,
            },
          ],
        },
      ])
    
      return (
          <RouterProvider router={router} />
      )
    }
    
    export default App

    <Home> and <About> will now be mounted in the <Outlet /> section of <Layout>.

    Note that the createBrowserRouter() parent element can also accept an errorElement component. That will be rendered when no path matches the specified URL. You can use it like in the following snippet:

    const router = createBrowserRouter([
        {
          element: <Layout />,
          // your custom routing error component
          errorElement: <Page404 />,
          children: [
              // ...
          ],
        },
      ])

    Great! React Router DOM has been set up correctly!

    As you can see, the length and complexity of the router object grow as the number of pages increases. Time to address this problem with a React routing layer!

    Adding the routing layer

    Create a routes folder inside the src folder of your React project. This is where you will place the routing layer. Next, create a pathConstants.js file and initialize it this way:

    // ./src/routes/pathConstants.js
    
    const PathConstants = {
        HOME: "/",
        // other pages's paths...
        ABOUT: "about"
    }
    
    export default PathConstants

    This file will define the PathConstants object, which contains all the string route constants that your React app consists of. These strings must follow the route naming convention defined by react-router-dom.

    For example, you can define a parametric route as below:

    players/:id
    

    The following two routes will both match that route:

    • players/1
    • players/cristiano-ronaldo

    You can then retrieve the id parameter in the page component as shown here:

    import { useParams } from 'react-router-dom';
    
    function PlayerPage() {
      // retrieve the id param from the URL
      let { id } = useParams()
    
      // ...
    }

    From now on, the to prop of <Link> components will have to use strings from the PathConstants object. This will help you centralize the mapping logic. For example, take a look at the <Header> component:

    import { Link } from "react-router-dom"
    import "../styles/Header.css"
    import PathConstants from "../routes/pathConstants";
    
    export default function Header() {
        return (
            <header>
                <div className="header-div">
                    <h1 className="title"><Link to={PathConstants.HOME}>My React App</Link></h1>
                    <nav className="navbar">
                        <ul className="nav-list">
                            <li className="nav-item"><Link to={PathConstants.TEAM}>Team</Link></li>
                            <li className="nav-item"><Link to={PathConstants.PORTFOLIO}>Portfolio</Link></li>
                            <li className="nav-item"><Link to={PathConstants.ABOUT}>About</Link></li>
                        </ul>
                    </nav>
                </div>
            </header>
        )
    }

    Notice that all to props are not raw strings but depend on path constants.

    In detail, each route string in PathConstants is associated with a specific page. You can define these mappings in the routes array in an index.js file:

    // ./src/routes/index.js
    
    import React from "react"
    import PathConstants from "./pathConstants"
    
    const Home = React.lazy(() => import("../pages/Home"))
    // other page components...
    const About = React.lazy(() => import("../pages/About"))
    
    const routes = [
        { path: PathConstants.HOME, element: <Home /> },
        // other mappings ...
        { path: PathConstants.ABOUT, element: <About /> },
    ]
    
    export default routes

    Note that each page component is imported by the React.lazy() statement. This enables the code splitting feature, which allows React to split the build bundle into smaller parts. Instead of loading a single, heavy bundle once, React can now lazy load the page component chunks on demand when required. This prevents the application from being slow on first load, making it more efficient.

    To make lazy-loaded components work, you need to render them through the <Suspense> component. Update <Layout> accordingly:

    // ./src/components/Layout.js
    
    import { Outlet } from "react-router-dom"
    import Header from "./Header"
    import Footer from "./Footer"
    import { Suspense } from "react"
    
    export default function Layout() {
        return (
            <>
                <Header />
                <main>
                    <Suspense fallback={<div>Loading...</div>}>
                        <Outlet />
                    </Suspense>
                </main>
                <Footer />
            </>
        )
    }

    React will take some time to load the page component and render it. For this reason, you need to specify a fallback component. This will be shown during that time. Then, the app will render the page component in the layout as desired.

    You may have noticed that the routes array follows the format required by the createBrowserRouter()‘s children field. In this way, you can straightforwardly use it in App.js:

    "./App.js"
    
    import {
      createBrowserRouter,
      RouterProvider,
    } from "react-router-dom"
    import routes from "./routes"
    import Layout from "./components/Layout"
    import Page404 from "./pages/Page404"
    
    function App() {
      const router = createBrowserRouter([
        {
          element: <Layout />,
          errorElement: <Page404 />,
          // specify the routes defined in the
          // routing layer directly
          children: routes
        },
      ])
    
      return (
          <RouterProvider router={router} />
      )
    }
    
    export default App

    Et voilà! You just implemented a routing layer in React! All we have left to do is to test it out.

    Routing layer in action

    Let’s see the routing layer in action using a React demo project based on the steps defined earlier.

    Clone the GitHub repository of the demo app and launch it locally with the following commands:

    git clone https://github.com/Tonel/routing-layer-react
    cd routing-layer-react
    npm i
    npm start

    Otherwise, play with test the demo project on CodeSandbox:

    What to do when your React app grows big

    Suppose that your React app grows big and involves thousands of pages. Mapping all the routes in only two files would become cumbersome.

    To address that, you could create a paths subfolder inside routes and split pathConstants.js into multiple files. Each of those files will contain the path constants related to a subsection of your app. For example, you could divide the path strings logically or based on the layout of the page component they are related to.

    Similarly, you can define a mappings subfolder and split index.js into several files. Then, you could define the final routes array within an index.js top-level file that imports and uses all other sub-routes.

    This is what your new routing layer file architecture will look like:

    routes
        ├── paths
        │    ├── blogPaths.js
        │     .
        │     .
        │     .
        │    └── teamPaths.js
        ├── mappings
        │    ├── blogMappings.js
        │     .
        │     .
        │     .
        │    └── teamMappings.js
        └── index.js

    As you can see, a routing layer is also a scalable approach!

    Conclusion

    In this article, you learned what a routing layer is, some of the several benefits it can bring to your architecture, and how to build it in React. A routing layer is a portion of your frontend architecture that exposes what your SPA needs for routing between pages. It improves the performance of your app and centralizes the routing logic. Implementing it in React is not complex, and it only requires a couple of files. Here, you learned how to add a routing layer to a React app and saw it in action in a demo project.

    7 thoughts on “How to Build a Routing Layer in React and Why You Need It

    1. Excellent. By far, this is the most elegant approach to implement routing in a React App. This is Industry level. Thank you.

    2. Fabulous example and unlike so many others a working example!! Really helped me understand Routing and Layouts

    3. How do you go about linking to a parametric route using the path constants? Do you do a string replacement on the path constant (that seems awkward)?
      e.g. REPORT_ANALYSIS: “reports/:reportId/analysis”,

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.