White Label Web App With ReactJS and Webpack

Jaroslaw Marek
The Startup
Published in
7 min readSep 7, 2020
Multiple React and Webpack logos in varying colors on various unifor backgrounds

On one of my previous projects, our goal was to have one application that can be used in-house to produce multiple, differently branded end-products with similar (or mostly the same) functionalities.

The solution needed to be able to:

  • apply different styles/theme per brand
  • cater for content differences
  • cater for layout/structural and behaviour differences between brands — this may feel like a too big ask for a white label app but usually, it’s just a matter of time when the product folks will require that and having a way to enable this in a sensible way is a big win
  • produce multiple products ideally without a need to make custom code replacements or custom code generation at build time

In this post, I’ll describe the approach we took to build multiple different versions of a React app from one codebase.

Research

Creating white-label applications with ReactJS is not a new topic. There are multiple approaches and blog posts out there describing solutions ranging from runtime theming, through build-time theming to writing custom app generators.

Our challenge appeared to be very similar to the one Walmart had. I found their blog post a very interesting and inspiring read. Even though their approach is more fitted to larger companies and bigger apps than ours, some of the underlying principles stay the same.

None of these examples was enough to suit our requirements though, so once I figured out a (yet another) way to do it, I decided to share it.

A solution

My solution was based on the Webpack’s resolve.modules setting. It allowed us to have modules resolved at build time without providing a specific path to them in the imports. It also enabled us to have a default fallback location for modules that didn’t need to be customised for some brands.

#1 Base project
As the base for the working demo, I’m going to use one of the example React projects (the calculator) with removed react-scripts and combined with a minimalistic Webpack and Babel configuration for simplicity. With that, we get a project with the following structure as the starting point:

See it in this commit

and when you run npm run build you get an app that looks like that:

Base application

#2 Introduce resolve.modules
Let’s add the brands folder:

and make the following changes to the code (the diff ignores the moved CSS files for brevity):

See it in this commit

At this point what we get is exactly the same app just with a bit confusing folder structure ;) What happened is that we no longer use relative paths to include the CSS files and let Webpack resolve them as modules at build time. Let’s leverage that in the next step.

#3 Add more brands
Let’s add acme and calc_co as new brands and customise the buttons and display respectively for each.

See it in this commit
I chose to customise only the colours of the buttons for the Acme brand. Default on the left, acme on the right.
I chose to customise the colours and text alignment of the Calc Co display. Default on the left, calc_co on the right.

We’ll also need to update the webpack.config.js as follows:

See it in this commit

The thing to note in this step is that neither acme or calc_co have a full set of files. Thanks to the default fallback mechanism, they don’t need to. We told Webpack to first look for modules for the brand defined in the APP_BRAND environmental variable, then in the default fallback location and then, if it still didn’t find the module, it means it’s an external one so it will look for in the node_modules.

Now we can build the app targeting specific brand with APP_BRAND={brand_name} npm run build to get the following:

The results of `npm run build`, `APP_BRAND=acme npm run build` and `APP_BRAND=calc_co npm run build` respectively.

We can apply different styling to the brands. Cool, but that was just one of many requirements. Let’s get into some more interesting stuff next.

#4 Support structural & content differences
In order to further differentiate the brands, apart from the styling, we introduced, what we called, wrapper objects (yes, naming is hard ;)). Their purpose is to enable structural and content differences between brands while avoiding code duplication (or keeping it to a minimum). The idea is that the main business logic and rendering stay in the top-level components (not brand specific), but these components are parameterised via props so that it’s possible for them to look or behave differently depending on these parameters. We found it to be a clean way of keeping the brand-specific code reasonable in size and contained within the folder of the given brand.

Here’s an example:

New wrapper objects. See them in this commit.

Here’s how I parameterised the main components:

For the wrapper components to take effect you need to import them instead of the main components. Example from the index.js:

I used the AppWrapper to place the calculator display at the bottom for the Calc Co brand:

Default on the left, Calc Co on the right.

I used roman numbers for the Acme buttons:

Default on the left, Acme on the right.

I fished off with totally customised button layout for the Calc Co:

Default on the left, Calc Co on the right.

The end results are as below:

The results of `npm run build`, `APP_BRAND=acme npm run build` and `APP_BRAND=calc_co npm run build` respectively.

As you can see, using this approach, you can get significantly different apps with not much additional code. Once you have the main components parameterised, you can easily build apps with various combinations of them.

You can view the live end-products here: default, acme, calc_co.

Productionising the solution

IDE features
This kind of custom module resolution may confuse some IDEs as they might not be aware of the location of the branded modules.
Here’s how to give a hint to VS Code intellisense via jsconfig.json as an example:

With jsconfig there’s no option to make it dynamic and make it handle other brands, unfortunately, but even having VS Code to look into the default one is a plus.
If you happen to use TypeScript, there’s a way to make it better though (see below).

Using TypeScript
If you use TypeScript, you can use the same compilerOptions in your tsconfig.jsonas with the above jsconfig.json. See the TS docs for details.

Although there’s no official support for a tsconfig.js, there’s an npm package which enables you to have dynamic TypeScript config which would suit this use case well as you’ll be able to reference the process.env.APP_BRAND in it.

Using Babel
With the babel-plugin-module-resolver and using JavaScript configuration file, you can configure the custom brands paths as follows:

Using ESLint
You can make ESLint rely either on your Babel config (eslint-import-resolver-babel-module), TypeScript config (eslint-import-resolver-typescript) or Webpack config (eslint-import-resolver-webpack). There are options for any of your project setup ;)

Improved CSS handling
One could argue that it isn’t great that I copied whole CSS files in order to customise just a couple of colours. They would be correct. One way of improving that would be to use Sass and have shared files with the styles which would use Sass variables. These files could import the variables and these imports could rely on the same mechanism as described above. The only thing you would need to have brand-specific, in this case, would be the definitions of the variables.

Further improvements

One thing which is not great with the described solution is that, when you look at the imports themselves, it may be hard to know which import is an external module and which one is your own branded one.
The solution to this could be using the resolve.alias setting instead of the described resolve.module. The problem with resolve.alias is that currently it only accepts string values, so there’s no way to achieve the default fallback behaviour.
The good news is that Webpack starting from version 5 (currently in beta) will support multiple values for the aliases as an array.

Another way of improving that could be using tsconfig-paths-webpack-plugin, if you use TypeScript, and, instead of specifying module resolution in webpack.config.js, rely on paths defined in your tsconfig.json as they support multiple values for aliases. I haven’t tried that one yet though.

Conclusion

So now you know how to leverage Webpack’s resolve.module in order to have a white labelled React application and build multiple differently branded products from one codebase. I understand it’s just yet another way of achieving that but, hopefully, someone with similar requirements will find it useful.

The source code of the example project I created is available on GitHub. You can also view the live end-products here: default, acme, calc_co.

--

--