DEV Community

Anass Boutaline
Anass Boutaline

Posted on

Building an Automatic Routing System in ReactJS with react-router-dom and TypeScript

In this article, we embark on a journey to construct an automatic routing system within a ReactJS application using react-router-dom and TypeScript. Our quest begins with the familiar starting point of any React project: Create React App. With its simplicity and efficiency, Create React App provides the perfect launching pad for our endeavors. We'll seamlessly integrate TypeScript into our project, leveraging its static typing capabilities to catch errors early in the development process and enhance code maintainability.

The desired results

    RouteContainer.register({
        path: '/',
        element: <Store />,
        title: "Boutique",
        label: "Boutique"
    })

    RouteContainer.registreGroup({
        path: '/products',
        routes: [
            {
                path: '/best-selling',
                element: <BestSelling />,
                title: "Best Product Landing Page",
                label: 'My Product'
            },
            {
                path: '/newest',
                element: <Newest />,
                title: "Best Product Landing Page",
                label: 'My Product'
            },
            {
                path: '/:id',
                element: <SingleProduct />,
                title: "Single Product",
                label: "Single Product"
            },
        ]
    })
Enter fullscreen mode Exit fullscreen mode

This code will generate 4 routes as follow
/=> Home page will render <Store /> component
/products/best-selling => will render <BestSelling /> component
/products/newest => will render <Newest /> component
/products/{id} => will render <SingleProduct />component, and id will be accessible from props.navigation passed nativelly to the component.

Getting started

First, let's create a file (just a regular js file) where we will define our routes in the manner shown in the code snippet above. Let's assume that we already have four components: <Store />, <BestSelling />, <Newest />, and <SingleProduct />.

// File: src/Routes/routes.js

import RouteContainer from '../Routing/RouteContainer' // this is where we will handle the routing logic

// import your components here...



export default function () {

    RouteContainer.register({
        path: '/',
        element: <Store />,
        title: "Boutique",
        label: "Boutique"
    })

    RouteContainer.registreGroup({
        path: '/products',
        routes: [
            {
                path: '/best-selling',
                element: <BestSelling />,
                title: "Best Product Landing Page",
                label: 'My Product'
            },
            {
                path: '/newest',
                element: <Newest />,
                title: "Best Product Landing Page",
                label: 'My Product'
            },
            {
                path: '/:id',
                element: <SingleProduct />,
                title: "Single Product",
                label: "Single Product"
            },
        ]
    })
};

Enter fullscreen mode Exit fullscreen mode

When the RouteContainer.register(RouteProps) function is called, RouteContainer will save the route data into a static array, which will be used later to dispatch routes. Let's first define some types.

//File: src/Routing/RouteContainer.ts

// This is the shape of route config (route data)
type routeConfig = {
    path: string,
    element: React.JSX.Element | React.ReactElement,
    title: string | undefined,
    label?: string 
}
Enter fullscreen mode Exit fullscreen mode

and crafting the first version of the RouteContainer class.

//File: src/Routing/RouteContainer.ts

export default class RouteContainer {

    static rawRoutes: routeConfig[] = []

    static register (routeConfig: routeConfig): void {
       RouteContainer.rawRoutes.push( routeConfig );
    } 

Enter fullscreen mode Exit fullscreen mode

So now, the RouteContainer is just using a static array to hold our Route configs or data.

let's see how we are going to use this to render element based on route config.

for that we will create a dispach function inside RouteContainer.

//File: src/Routing/RouteContainer.ts

static dispatch () {
        return RouteContainer.rawRoutes
    }
Enter fullscreen mode Exit fullscreen mode

in this function we are just returning the rawRoutes array, basically this array will be used by useRoutes from react-router-dom to render routes:

// File: App.js

function App() {
  return (
        <BrowserRouter>
          <AppLauncher />
        </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
// File AppLauncher.js

import { useRoutes } from "react-router-dom"
import RouteContainer from "./Routing/RouteContainer"
import registerRoutes from "../Routes/routes"

export default function RouteDispatcher () {

    registerRoutes() // register routes in RouteContainer 


    return useRoutes([].concat( RouteContainer.dispatch() /** Dispatch registred route inside RouteRendrer */))



}
Enter fullscreen mode Exit fullscreen mode

We can finally add registerGroup to RouteContainer:

//File: src/Routing/RouteContainer.ts

type routeGroupConfig = {
    routes: Array<routeConfig>,
    path: string
}

...

export default class RouteContainer {

...

static registreGroup ( routeGroupConfig: routeGroupConfig ) {
        routeGroupConfig.routes.forEach(routeConfig => {
            RouteContainer.register( {
                ...routeConfig,
                path: RouteContainer.prepareAndMergePaths( routeGroupConfig.path, routeConfig.path )
            } )
        });
    }


private static prepareAndMergePaths ( path: string, anOther: string  ) {
        return RouteContainer.preparePath( path ) + RouteContainer.preparePath( anOther )
    }

 private static preparePath(str: string) : string{
        if( str.length === 0 ) {
            return ""
        }
        str = str.endsWith('/') ? str.slice(0, -1) : str;
        str = str.startsWith('/') ? str.slice(1, str.length) : str;

        return str + "/"
    }

Enter fullscreen mode Exit fullscreen mode

Yeah, this was easy. but the question is why we are doing this?

I'm doing this to keep my routes organized in a single place, for example, in a routes.js file, making it easy to remove, add, or update routes in my app. However, the best part is when each route needs specific contexts. If you find this helpful, comment, and I will post a new article on how to use this architecture to automatically inject providers and layouts into each route.

Top comments (0)