Building a Multi-Tenant React App. Part 3: Customizable Components
Tags: react, javascript, webdev
Published: 2020-05-14 17:41:19

---
title: Building a Multi-Tenant React App. Part 3: Customizable Components
published: true
date: 2020-05-14 00:00:00 UTC
tags: react, javascript, webdev
canonical_url: https://www.delvalle.dev/posts/multi-tenant-react-app-part-3/multi-tenant-react-app-part-3/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/7exgplrn0529h69yx02g.jpg
---

Hello again! I've been quite busy lately and haven't been able to keep up with this series. Let's try to go back to where we were and keep building this app. I think this one's gonna be short. In case you haven't read the previous articles here they are:

- [Part 1: Setup](https://delvalle.dev/posts/multi-tenant-react-app-part-1/multi-tenant-react-app-part-1/)
- [Part 2: Dynamic Routes](https://delvalle.dev/posts/multi-tenant-react-app-part-2/multi-tenant-react-app-part-2/)

Today I'm gonna talk about customizable components and how you can render client-specific stuff in the UI based on configuration. We already know how to render different routes for the different clients but what about different components and components that are flexible enough to look and feel different?

So, starting where we left off in the previous article. **Client A** shows a **HomePage** component in the root route whereas **Client B** shows a **ProductPage** component. Let's say **Client A** wants to show a list of products in the **HomePage** via some cards. **Client B** will show a featured product in the **ProductPage** and some cards below it for the related products.

These cards will look different for these clients but we'll use the same component. This component will receive some config from the server that will tell it how to look.

## [<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-card-configuration)Adding the Card configuration

We'll add a new _components_ node in our config objects, right next to the routes node. We don't want to nest the component configurations inside the routes configuration cause the entire config object could get pretty deep and the components don't care in which route they are.

The card for **Client A** will have a header, an image, a footer, and will show a title and description. For **Client B** it will not show the header.

This is how our config objects will look now:

```js
[
  {
    "clientId": 1,
    "name": "Client A",
    "routes": {
      "home": {
        "path": "/",
        "component": "HomePage"
      },
      "product": {
        "path": "/product/:productId",
        "component": "ProductPage"
      }
    },
    "components": {
      "card": {
        "showHeader": true,
        "showImage": true,
        "showFooter": true
      }
    }
  },
  {
    "clientId": 2,
    "name": "Client B",
    "routes": {
      "home": {
        "path": "/",
        "component": "ProductPage"
      }
    },
    "components": {
      "card": {
        "showHeader": false,
        "showImage": true,
        "showFooter": true
      }
    }
  }
]
```

## [<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>](#creating-the-config-context)Creating the Config Context

So now we'll have components that will receive their configuration. It would be useful to use React's Context API so we can have our entire client configuration in one single place. It can then be accessed by any component that needs it.

We'll create a new folder inside **src** called **context** and will add a new file called **Config.js** with the following code:

```js
import { createContext } from 'react';

const initialConfig = {
  name: "No name",
  routes: {},
  components: {}
};

const ConfigContext = createContext(initialConfig);

export default ConfigContext;
```

What we are doing here is creating a new empty context that will store our config object. We'll leave those values empty for now but this is the place where you would want to add default configuration properties in case they are missing in the config that comes from the backend.

Now in **App.js** we need to import this context. We also need to wrap the Routes component in a Config Context Provider, like so:

The **value** prop in ConfigContext.Provider will receive the config we got from the server. This config will now be accessible to any component down the tree that makes use of **useContext** to access...well...the config context.

The entire **App.js** file will now look 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';
import ConfigContext from './context/Config';

function App() {

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

  const { loading, data } = config;

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

export default App;
```

## [<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-the-card-component)Implementing the Card Component

Now that we have our Config Context ready, we just have to start consuming it in our components. The Card component will use **useContext** to get access to it and will retrieve the card-specific configuration. It will render according to the variables in that configuration.

```js
import React, { useContext } from 'react';
import ConfigContext from '../context/Config';

function Card({ title, description, img }) {

  const { components: { card }} = useContext(ConfigContext);
  const { showHeader, showImage, showFooter } = card;
  
  return (
    <div className="card-container">
      {
        showHeader && (
          <div className="card-header">
            <h4 className="card-title">
              {title}
            </h4>
          </div>
        )
      }
      {
        showImage && (
          <img className={!showHeader ? "card-image-rd" : "card-image"} src={img} alt="Card Img" height="240" width="320"/>
        )
      }
      {
        showFooter && (
          <div className="card-footer">
            <p className="card-description">
              {description}
            </p>
          </div>
        )
      }
    </div>
  );
}

export default Card;
```

I'll add the card styles at the end of the article so we can keep going with what's important.

The Card component will show or hide the header, footer, and image based on the configuration it receives from the context.

You can see that the Card component is receiving a title, a description, and an image from props. We have to define these somewhere and we also need to make use of this component. We'll go to the **HomePage** and **ProductPage** and add it there.

We'll add an array of items in our **HomePage** component. We'll loop through this array and return a card for each item. It will end up looking like this:

```js
import React from 'react';
import Card from './Card';

const items = [
  {
    id: 'card1',
    title: 'First Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=1"
  },
  {
    id: 'card2',
    title: 'Second Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=2"
  },
  {
    id: 'card3',
    title: 'Third Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=3"
  }
]

function HomePage({ items }) {
  
  return (
    <div>
      Welcome to the Home Page!
      <div className="cards-container">
        {
          items.map((item) => (
            <Card key={item.id} {...item} />
          ))
        }
      </div>
    </div>
  );
}

HomePage.defaultProps = {
  items
}

export default HomePage;
```

Normally, we'll receive those items from the server but we keep this simple for now.

As for the **ProductPage** we'll do something a little different. Let's say **Client B** decided to show a featured product more predominantly and some cards below it for the other products.

For this, we're still gonna have an array of items but one of them will have a featured flag set to true. We are gonna render the featured item above the other ones. The component will look something like this:

```js
import React from 'react';
import Card from './Card';

const items = [
  {
    id: 'card0',
    title: 'Featured Product',
    description: 'Interesting description',
    img: "https://loremflickr.com/320/240/food?random=0",
    featured: true
  },
  {
    id: 'card1',
    title: 'First Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=1"
  },
  {
    id: 'card2',
    title: 'Second Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=2"
  },
  {
    id: 'card3',
    title: 'Third Card',
    description: 'Some boring description',
    img: "https://loremflickr.com/320/240/food?random=3"
  }
]

function ProductPage({ items }) {

  const featuredProduct = items.find((item) => item.featured === true);
  const relatedItems = items.filter((item) => !item.featured)
  
  return (
    <div>
      Welcome to the Product Page!
      <div className="featured-product">
        <div>
          <img className="featured-img" src={featuredProduct.img} alt="Featured Img" height="240" width="320"/>
        </div>
        <div className="featured-content">
          <h2>{featuredProduct.title}</h2>
          <p>{featuredProduct.description}</p>
        </div>
      </div>
      <div className="cards-container">
        {
          relatedItems.map((item) => (
            <Card key={item.id} {...item} />
          ))
        }
      </div>
    </div>
  );
}

ProductPage.defaultProps = {
  items
}

export default ProductPage;
```

So what we are doing is extracting the featured item and the rest of the items into separate variables so we can render them separately.

Now, before running the app to verify how all this looks, let's add some styles in **index.css** :

```css
.cards-container {
  display: flex;
  margin: 20px 0;
}

.card-container {
  width: 320px;
  margin: 5px;
  background-color: white;
  color: black;
  border-radius: 20px;
  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

.card-header, .card-footer {
  padding: 10px;
}

.card-title, .card-description {
  margin: 0;
}

.card-image-rd {
  border-top-left-radius: 20px;
  border-top-right-radius: 20px;
}

.card-description {
  font-size: 18px;
  text-align: left;
}

.featured-product {
  display: flex;
  margin-top: 20px;
  background-color: white;
  color: black;
  border-radius: 20px;
  height: 240px;
  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

.featured-img {
  border-top-left-radius: 20px;
  border-bottom-left-radius: 20px;
}

.featured-content {
  padding: 10px;
  text-align: left;
}
```

## [<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>](#run-the-app)Run the app

Just like in the other articles. We have to run the server in one terminal and each of the clients separately using the following commands:

```
npm run server
```

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

This is how **Client A** should look:

[![client-a](https://www.delvalle.dev/static/370fe714208a4b53c4fbfea0a0923f52/35c67/client-a.png)](/static/370fe714208a4b53c4fbfea0a0923f52/f4055/client-a.png)

And this is how **Client B** should look:

[![client-b](https://www.delvalle.dev/static/bad59923d346f48b674535f3732ac5eb/35c67/client-b.png)](/static/bad59923d346f48b674535f3732ac5eb/a987b/client-b.png)
  

And that's it! We now have a basic multi-tenant app that can render different routes and components based on configuration received from the backend. As I mentioned in the first article, the real world multi-tenancy project I worked in was a little bit more complex but the basic ideas and architecture remain.

This approach should be enough to work on a larger app and gives space to work in more customization. Any route and any component can be customizable following this approach. Remember, if you want to add default configuration you can do so where we created the Config Context in the **initialConfig** object. This way if there is a client that doesn't have any configuration coming from the backend you can rely on the defaults.

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)_