Building a Multi-Tenant React App. Part 2: Dynamic Routes
Tags: react, webdev, javascript
Published: 2020-04-15 16:12:03

---
title: Building a Multi-Tenant React App. Part 2: Dynamic Routes
published: true
date: 2020-04-15 14:21:00 UTC
tags: react, webdev, javascript
canonical_url: https://www.delvalle.dev/posts/multi-tenant-react-app-part-2/multi-tenant-react-app-part-2/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/brpkvnuoeyzv5fd3jxex.jpg
---

Hello! Welcome to the second part of my Multi-Tenant React App series. In the [first part](https://delvalle.dev/posts/multi-tenant-react-app-part-1/multi-tenant-react-app-part-1/) we took a look at how to set up the project and implemented some simple _multi-tenancy_ features.

This series is inspired in a real-world problem that I faced a couple of years ago -_although a bit more complex than this_-. My team and I had to develop a multi-tenant app that could look and behave differently based con client-specific configuration coming from the API. Still, most of the layout, styles, and features would be the same for all the clients.

Imagine now that **Client A** wants to have a home page in their root route that will show a list of their products, whereas **Client B** wants to show a featured product page in their root route. Both clients want an about page and didn't request any custom look or feature in it.

This would mean two very different components to show in the root route and one common component for the about.

Let's leverage our current architecture to accomplish this.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#adding-the-routes-configuration)Adding the routes configuration

First, we are gonna add some new config to our JSON database. Each client will have its own set of custom routes and the client side will render them accordingly.

```js
[
  {
    "clientId": 1,
    "name": "Client A",
    "routes": {
      "home": {
        "path": "/",
        "component": "HomePage"
      },
      "product": {
        "path": "/product/:productId",
        "component": "ProductPage"
      }
    }
  },
  {
    "clientId": 2,
    "name": "Client B",
    "routes": {
      "home": {
        "path": "/",
        "component": "ProductPage"
      }
    }
  }
]
```

So, we've added a new **routes** object, each of its nodes will be specific to a route.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#implementing-react-router)Implementing React Router

We will need to install `react-router-dom` in our client-side so we can handle these routes. Open the terminal and go to the **client** folder and execute the following command:

```
npm i react-router-dom
```

We are now gonna create a new component called **Routes** which will use **react-router**. This component will receive the routes config object we added to the database, will iterate through them and render their respective **Route** components.

```js
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom";

function Routes({ routes }) {

  return (
    <Router>
      <Switch>
        {
          Object.keys(routes).map((key) => {
            const route = routes[key];
            return (
              <Route 
                key={`route-${route.path}`}
                path={route.path}
                exact>
                  <div>
                    {route.component}
                  </div>
              </Route>
            )
          })
        }
      </Switch>
    </Router>
  );
}

Routes.defaultProps = {
  routes: []
}

export default Routes;
```

So, instead of hardcoding our **Route** components, we render them dynamically based on what we receive in the routes config.See that I've set the routes prop as an empty array in **Routes.defaultProps** so it doesn't crash if we don't receive any routes from the server. We'll use this later to define a set of default routes.

Another important thing to consider for now is that we are not rendering any actual component in these routes, just the component name so we can test this before moving on.

Now, let's go to the **App** component and implement the **Routes** component we just created. I've made some changes to the JSX and will now show the react logo while waiting for the config objects. If the request is successful our **Routes** component will receive the routes config and render the routes as expected.

The **App** component will now look something like this:

```js
import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';
import { getConfig } from './services/config.service';
import Routes from './Routes';

function App() {

  
  const [config, setConfig] = useState({ loading: true, data: {} });
  const { loading, data } = config;

  useEffect(() => {
    async function getConfigAsync(){
      const { data } = await getConfig();
      setConfig({ data });
    }
  
    getConfigAsync();
  }
  , []);
  
  return (
    <div className="App">
      <header className="App-header">
          {
            loading && <img src={logo} className="App-logo" alt="logo" />
          }
          {
            data.error && <p>'Error getting config from server'</p>
          }
          
          <Routes routes={data.routes}/>
      </header>
    </div>
  );
}

export default App;
```

Ok, let's go and run the server and two client-side instances so we can test the two different configurations. Client A should show "HomePage" on the root route and Client B should show "ProductPage" for that same route.

In the project root folder let's do:

```
npm run server
```

And then move to the **client** folder. Open two terminals here so you can run the two client instances:

```
REACT_APP_CLIENT_ID=1 npm start
```

And:

```
REACT_APP_CLIENT_ID=2 npm start
```

You should see the following screen for **Client A** :

[![drawing](https://www.delvalle.dev/static/6de9360f2b0e42cf75e8352da17e709c/a5cd6/client-1-a.png)](/static/6de9360f2b0e42cf75e8352da17e709c/a5cd6/client-1-a.png)

And this one for **Client B** :

[![drawing](https://www.delvalle.dev/static/fa204e664eef0da4582230e8b4cd9c93/94a55/client-2-a.png)](/static/fa204e664eef0da4582230e8b4cd9c93/94a55/client-2-a.png)

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#rendering-components-dynamically)Rendering components dynamically

So, now that we are rendering the routes correctly we need to add an object in the client-side that will map a component name like **HomePage** and **ProductPage** to an actual component.

Let's add these two new components first. Create a **components** folder next to App.js and add the following code. We'll keep them simple for now:

HomePage.js

```js
import React from 'react';

function HomePage() {
  
  return (
    <div>
      Welcome to the Home Page!
    </div>
  );
}

export default HomePage;
```

ProductPage.js

```js
import React from 'react';

function ProductPage() {
  
  return (
    <div>
      Welcome to the Product Page!
    </div>
  );
}

export default ProductPage;
```

We now need to add an object that will map the component name we have in the config object with the real component. Right here in the **components** folder, add a **componentMapper.js** file with the following code:

```js
import HomePage from './HomePage';
import ProductPage from './ProductPage';

const COMPONENTS = {
  'HomePage': HomePage,
  'ProductPage': ProductPage
}

export default COMPONENTS;
```

We are now gonna use this mapper in our **Routes** component so each route renders its specific component.

Let's import the **COMPONENTS** map in the **Routes** and do some quick changes in the render function. We have to get the component from the map and then render it inside the **Route** component, like so:

```js
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom";
import COMPONENTS from './components/componentMapper';

function Routes({ routes }) {

  return (
    <Router>
      <Switch>
        {
          Object.keys(routes).map((key) => {
            const route = routes[key];
            const Component = COMPONENTS[route.component];
            return (
              <Route 
                key={`route-${route.path}`}
                path={route.path}
                exact>
                  <Component />
              </Route>
            )
          })
        }
      </Switch>
    </Router>
  );
}

Routes.defaultProps = {
  routes: []
}

export default Routes;
```

You should see the following screen for **Client A** :

[![drawing](https://www.delvalle.dev/static/eff72c6cb2ba782b1fcb2229be0cd4c7/92415/client-1-a2.png)](/static/eff72c6cb2ba782b1fcb2229be0cd4c7/92415/client-1-a2.png)

And this one for **Client B** :

[![drawing](https://www.delvalle.dev/static/dbabce301847c369e0b2a9674a8522eb/7cc4b/client-2-a2.png)](/static/dbabce301847c369e0b2a9674a8522eb/7cc4b/client-2-a2.png)

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#default-routes)Default Routes

As the last step for today's post, we'll add support for default routes. This means that there will be routes that are common among clients. We'll have a set of default or common routes in our client-side so they don't have to be added for all the clients in their config objects.

We will need to add a **DEFAULT\_ROUTES** object in our **Routes** component:

```js
const DEFAULT_ROUTES = {
  about: {
    path: "/about",
    component: "AboutPage"
  },
}
```

And set them as the default value for the **routes** prop in **Routes.defaultProps** :

```js
Routes.defaultProps = {
  routes: DEFAULT_ROUTES
}
```

But this is not enough if we want to include the **about** route along with the custom ones, we have to merge both objects, the custom one from the config and the default one. I'll also add a simple navigation menu so we can go to the **About** page. The **Routes** component will end up being something like this:

```js
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  NavLink
} from "react-router-dom";
import COMPONENTS from './components/componentMapper';

const DEFAULT_ROUTES = {
  about: {
    path: "/about",
    component: "AboutPage"
  },
}

function Routes({ routes: customRoutes }) {

  // We'll now call the routes prop as customRoutes inside the component.
  // Merge customRoutes with the default ones.
  const routes = {...customRoutes, ...DEFAULT_ROUTES};

  return (
    <Router>
      <nav>
        <ul>
          <li>
            <NavLink to="/" activeClassName='active' exact>Home</NavLink>
          </li>
          <li>
            <NavLink to="/about" activeClassName='active' exact>About</NavLink>
          </li>
        </ul>
      </nav>
      <Switch>
        {
          Object.keys(routes).map((key) => {
            const route = routes[key];
            const Component = COMPONENTS[route.component];
            return (
              <Route 
                key={`route-${route.path}`}
                exact
                path={route.path}>
                  <Component />
              </Route>
            )
          })
        }
      </Switch>
    </Router>
  );
}

Routes.defaultProps = {
  routes: DEFAULT_ROUTES
}

export default Routes;
```

I added the following styles to **index.css** so the Nav Bar would look OK:

```css
.nav-bar {
  width: 100%;
  position: fixed;
  top: 0;
}

.nav-bar ul {
  list-style-type: none;
  margin: 0;
  padding: 15px;
  display: flex;
  justify-content: flex-end;
}

.nav-bar ul li {
  margin: 10px;
}

.nav-bar ul li a{
  text-decoration: none;
  color: white;
}

.nav-bar ul li a.active{
  color: cornflowerblue;
}
```

Cool! So, now you should be able to navigate between routes and the **About** page will be available for both clients.If we wanted to show custom information for each client in the **About** page we would need to fetch that from the server but we'll leave it as is for now. Remember that for client B the Home route shows the **ProductPage** instead of the **HomePage** component.

* * *

That's all for now! We've now covered custom routes but we still have to cover customizable components based on the config. I'll leave that for the next post.

Here's the [Github repo](https://github.com/dlvx/multi-tenant-react-app) in case you want the whole project.

Stay tuned and thanks for reading!

_Follow me on twitter: [@jdelvx](https://twitter.com/jdelvx)_