DEV Community

Warren
Warren

Posted on

Multi Tenanted Content changes in React

Introduction

We have a number of tenants who require the same basic functionality but use slightly different terminology leading to a requirement for our user interface to vary based on which tenant is being used. It turns out this is very easy to do in React with the assistance of React-Intl.

Notes

I use yarn as my primary package manager so expect yarn commands to be quoted throughout. If you use npm or something else you'll need to translate the commands. If this is more complicated than using npm install ... instead of yarn add ... I'll try to add more information.

I'm going to be using Windows, VS Code, PowerShell for my development work, but it is my intention that this tutorial will apply regardless of what comparable tools you will use.

Setup

You can retrieve the project files from Github. Starting at the second commit in the project we have a simple form with no content management in place. All the strings are hard coded parts of the components. We're going to work through from here to change that, or you can just look at the finished thing.

A simple form with some strings to customise

To compile and view the page with the dev server navigate to the directory and run yarn start. This will watch for file changes and refresh the browser window with the latest version.

React-Intl

React-Intl is primarily intended for internationalising your app, but it turns out we can also use it to achieve the above without compromising it's original purpose if you also require internationalisation.

Adding react-intl

Add react-intl to your project by running yarn add react-intl. We're now going to add react-intl to our app so that the components can use it for managing the string content.

In index.js add the following line

import { IntlProvider } from 'react-intl';

We then need to wrap our entire app in the IntlProvider HOC.

<IntlProvider locale="en">
    <App />
</IntlProvider>

Our first managed string

Now that we've added react-intl we can use it with the FormattedMessage component that is provided by react-intl. In ExampleForm.jsx add the following

import {FormattedMessage} from 'react-intl'

The first string we're going to change here is the label for the text input. Currently it is "Procedure name". It still will be by the time we're done but it will be being managed by react-intl enabling us to change it.

Add a new const above the ExampleForm as follows

const messages = {
  procedureNameLabel: {
    id: "ExampleForm.ProcedureName.Label",
    defaultMessage: "Procedure Name",
    description: "Label text for the procedure name input text box on the Example form"
  }
}

(Note: The description is optional, but you must provide a unique id and defaultMessage for all messages you use with react-intl).

Now replace the text of "Procedure Name" in the label markup so that it looks like the following

<label htmlFor="procedureName">
    <FormattedMessage {...messages.procedureNameLabel} />
</label>

The end result should be appear exactly the same as before. It isn't quite the same since the FormattedMessage renders the text within a <span />. Inspecting the html should reveal the following

<label for="procedureName">
    <span>Procedure Name</span>
</label>

This may mean you need some changes to your css you can use the following alternative approach

Plain formatted message

Create a new file called PlainFormattedMessage.jsx and put the following in to it

import React from 'react';
import { FormattedMessage } from 'react-intl'

const PlainFormattedMessage = (props) => 
    <FormattedMessage {...props}>
        {(message) => message }
    </FormattedMessage>

export default PlainFormattedMessage

This will render the message without the <span /> tag surrounding it. You can use it be replacing import { FormattedMessage } from 'react-intl' with import PlainFormattedMessage from './PlainFormattedMessage' and changing <FormattedMessage ...> to <PlainFormattedMessage ...> in the ExampleForm.jsx file.

Changing strings in attributes

The above approaches work well where the string is the child of a tag, but for situations where we want to change the text of an attribute we need to use a new component in a very similar way to the PlainFormattedMessage. The example we're going to use here is the value attribute of the submit button. Add a new value to the messages object for the text.

exampleFormSubmitProcedureButtonText: {
    id: "ExampleForm.SubmitProcedure.ButtonText",
    defaultMessage: "Submit procedure"
}

Then replace the <input ... /> with the following

<FormattedMessage {...messages.exampleFormSubmitProcedureButtonText}>
    {(message) => <input type="submit" value={message} />}
</FormattedMessage>

Again if we've done everything right it should still compile and be rendering exactly the same is it was before. It's a lot of work for no changes so far, but our next steps are where we'll override the defaults and start to bring value from these changes.

Note: You probably want to move this functionality out to a separate component. We have an <Input /> component specifically for rendering our <input />s and have included this functionality there by passing the message as a property to that component, as well as a tooltip attribute which also uses a managed string.

Changing the strings

So our new tenant requires us to label the procedureName input text box with the text "Operation Name" instead of "Procedure Name". In fact everywhere where we've said "Procedure" they want us to say "Operation" so we override the current default messages. We can do this by creating a file that has all of the overriden strings in it. You don't have to retrieve these overrides from a file, you could use fetch() and retrieve them at run time if that suits your use case.

Create a new file and call it something that identifies the tenant. I'm going with op-tenant.json for my example.
The key's should correspond to the id of the message we wish to override, while the value should be the new text, so for our two string's so far it should look like this:

{
    "ExampleForm.ProcedureName.Label": "Operation Name",
    "ExampleForm.SubmitProcedure.ButtonText": "Submit Operation"
}

In index.js we'll need to import/retrieve the tenant message overrides. They then need to be passed to the IntlProvider on the messages attribute if that tenant has been selected. I'm going to assume you already have some means of determining your tenant and in the example I'm just going to set it using a constant string value. Now we need a method which will return the overrides if the tenant matches.

import opTenantMessages from './messages/op-tenant.json'

const tenant = "normal-tenant"
const getMessages = () => {
    if (tenant && tenant === "op-tenant")
        return opTenantMessages
    return {}
}

Then to use it we change the <IntlProvider> so that it uses this method

<IntlProvider locale="en" messages={getMessages()}>

With the above our site should still render as it was before.
A simple form with some strings to customise
But just by changing the tenant value to "op-tenant" it should update the strings
A simple form with some strings to customise

Final thoughts

I recommend you replace all string content with messages whether your app is tenant or international or not. One day one of these might become a requirement and you'll be glad that the foundations are already in place. It also means that string content isn't cluttering your markup.

The FormattedMessage used here is just one of the components provided by react-intl. Most of the others are more useful for internationalisation, such as managing currency symbols, etc. Another which can be particularly useful for tenancy applications is FormattedHtmlMessage which allows the message to contain HTML markup which can then be overriden by tenant.

String interpolation or rather template strings are also supported by react-intl should you need to change a string made of other variables. For example error messages which need to change based on the field with the error. If the defaultMessage contained a template string resembling the following

`The {fieldName} is a required field`

We would need to provide a fieldName to the component that is rendering the message as follows

<FormattedMessage {...messages.errorMessage} values={ fieldName: "Date of birth"} />

This would then output "The Date of birth is a required field". I'll leave it as an exercise for the reader to figure out how to also override the fieldName with a react-intl controlled string.

Hopefully I've provided you with enough to enable you to have your application changing content based on the tenant.

Top comments (3)

Collapse
 
akari0624 profile image
Morris-Chen

I think the last piece of code:

<FormattedMessage {...messages.errorMessage} values={ fieldName: "Date of birth"} />

should be

<FormattedMessage {...messages.errorMessage} values={{fieldName: "Date of birth"}} />

the values property of FormattedMessage accept an object, so it should be {{.......}}, the outer { } is the JSX syntax

Collapse
 
browndini profile image
Kyle Brown

have you looked into i18n.

Collapse
 
wozzo profile image
Warren

I hadn't but at first glance it looks fairly easy to achieve the same as here but you'd need to override the values within the translation before they're passed in to the init method.