Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

White-labeling React apps

White-labeling React apps Photo by Cyril Saulnier

A white label app is an app that we build once and "resell" it to other people/companies. Very often we are talking about applying different themes but sometimes we have to change logic too. Such changes should be as declarative as possible so they scale well. Otherwise is more of a copy/paste exercise. In this article I want to sketch out a couple of approaches for white labeling in React applications.


The white labeling could be done either at runtime or at build time. At runtime the solution relies on patterns and logic while the build time approach is based on tooling.

Let's dive in and see some examples.

White labeling at runtime

What I mean by runtime is that we have one bundle that contains all the necessary styling for all the different variations. When the app runs, it takes a decision which one to use.

The first and the most obvious solution for such design is the context API.

Using React context

The context API in React is perfect for delivering settings/configuration to components. Actually, the official documentation touches on exactly a theming problem.

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  const theme = window.location.href.includes('brandA') ?
    themes.dark :
    themes.light;
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

Look at the <App> component. That's the point of the app where we have to make the decision which of the light or dark theme will be used. In this particular example we are relying on the current URL.

Similar effect could be achieved by using patterns like higher-order component or function-as-child-component.

Using a composition pattern

Let's say that we need the same <ThemedButton>. We want to apply different styling based on the current URL. Without using the context API we may do it like that:

function GetTheme({ children }) {
  const theme = window.location.href.includes('brandA') ?
    themes.dark :
    themes.light;
  return children(theme)
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <GetTheme>
    {
      theme => (
        <button style={{ background: theme.background, color: theme.foreground }}>
          I am styled by theme context!
        </button>
      )
    }
    </GetTheme>
  );
}

Since React hooks are here for a while we may also decide to use them too. We may create a custom hook called useTheme that will check the current URL and will bring back the necessary styling.

That's all working fine if we have to deal with just colors, fonts and sizes. However, it becomes a bit more complicated if we need to change the layout, add or remove components. Even worst if we have to write a completely different logic. Then we will probably end up using if-else statements. Which doesn't scale very well. For such cases I recommend checking out the second approach - white labeling done at build time.

At build time

The idea here is to solve the problem before the app reaches the user. Meaning that we have to produce different bundles for the different variations. So, at the end the JavaScript that is shipped to the browser is only what the user needs to see. I have only experience with Webpack so I'm going to show you how the things look like there.

The magic happens via the following resolver plugin:

const resolverCache = {};
const WHITELABEL_DIR_NAME = '__whitelabel__';

const CustomResolver = (brand) => ({
  apply: function (resolver) {
    const target = resolver.ensureHook('resolve');

    resolver.getHook('resolve').tapAsync('ThemeResolverPlugin', (request, resolveContext, callback) => {
      const filePath = path.join(request.path, request.request);
      const swap = (overwrites) => {
        const newRequest = Object.assign({}, request, overwrites);
        console.log(
          `[Whitelabel] ${path.basename(request.request)} swapped with ${WHITELABEL_DIR_NAME}/${path.basename(
            newRequest.request
          )}`
        );
        resolver.doResolve(target, newRequest, null, resolveContext, callback);
      };

      if (filePath.indexOf('node_modules') < 0) {
        if (resolverCache[filePath]) {
          swap(resolverCache[filePath]);
          return;
        }
        const brandedFilePath = convertToWhiteLabelPath(filePath, brand);
        const exists = fs.existsSync(ensureExtension(brandedFilePath));
        if (exists) {
          resolverCache[filePath] = {
            path: path.dirname(brandedFilePath),
            request: './' + path.basename(brandedFilePath),
          };
          swap(resolverCache[filePath]);
          return;
        }
      }
      callback();
    });
  },
});

I'm not going to explain the plugin line by line because I don't know much about the Webpack plugin API. The gist is that we check whether there is __whitelabel__/<file> alternative of each file. And if yes then we instruct Webpack to use it. For example:

/components
  /ui
    /buttons
      /__whitelabel__
        /ThemedButton.brandA.jsx
      /ThemedButton.jsx

When we use the plugin via the following lines

resolve: {
  plugins: [CustomResolver('brandA')]
}

we will get ThemedButton.brandA.jsx in the produced bundle and not ThemedButton.jsx.

Conclusion

I personally prefer the second at-build-time approach. Mainly because it keeps the original code clean. I'm also seeing it used by some big brands which makes me think that it's probably a good choice.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.