Building an Automatic Routing System in ReactJS with react-router-dom and TypeScript
Published: 2024-03-17 01:07:02

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"
            },
        ]
    })
```

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"
            },
        ]
    })
};

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

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

```
// 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 */))


    
}
```
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 + "/"
    }

```
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**.