Sitemap

React: How to Scale Theme Sets with Deep Merging

Create sets of themes and inject them as one object into React theme providers

8 min readJun 6, 2021

--

Press enter or click to view image in full size

Introduction

In this article I will share a deep merging solution for adding sets of themes to your React apps. By deep merging multiple theme objects hosted in different files that adhere to a common theme structure, you can keep sets of themes separate and scale your theme capabilities with ease.

I implemented this solution on my company website’s landing page, which already had light and dark themes built in prior to the implementation.

What I aimed to do was introduce Cryptocurrency based themes without having to disrupt or refactor the default themes. The problem solved was not to mix Cryptocurrency based themes (Bitcoin, Ethereum and DOT) with the default light and dark themes.

In other words, the Crypto themes needed to exist in a separate file to the default themes.

To the end-user though, the themes are all displayed together and can be seamlessly switched between from any component. The components and UX are completely de-coupled from the underlying theming structure, as the following screenshots demonstrate:

Press enter or click to view image in full size
Active DOT theme with theme menu and DOT icon next to JKRB logo
Press enter or click to view image in full size
Active ETH theme (notice the icon update next to the JKRB logo)
Press enter or click to view image in full size
Active dark theme — with all Cryptocurrency branding removed

There is no limit to how many “theme files” you introduce into your app.

These theme files we will be discussing are all deep merged (as opposed to shallow merging) into one theme object before being injected into ThemeSet objects, that will ultimately be imported and used in component styling.

Thus, you can introduce more themes as and when you see fit without having to navigate or refactor other theme files. With this approach, theme files become lightweight and easy to manage — they could also be autogenerated if you regularly update theme structure.

React theming intuition: pre-requisite reading

The theming context used here is provided by the Styled Components ThemeProvider component and the theme values to be imported into your components rely on Styled Theming ThemeSet objects.

To read more about how to integrate Styled Theming and Styled Components into your React projects, refer to my published article: React Dark Mode with Styled Theming and Context.

If you are developing a React Native application, the deep merging technique discussed here can also be applied to such projects. To understand how theming is applied in React Native specifically, refer to my published article: React Dark Mode with Styled Theming and Context.

Our multi theme-file approach in this piece builds upon these theming foundations.

The Solution: High Level Overview

One can think of this solution as a 3 step process.

  • Step 1: Define your theme configurations in separate files.
  • Step 2: Import all theme configurations and merge into one object.
  • Step 3: Import merged object and spread properties into ThemeSet objects. It is these objects that are imported into your components to determine a value based on the currently active theme

For a general intuition, the following screenshot outlines the 4 files at play in this 3 step implementation:

Press enter or click to view image in full size
Merging `default` and `crypto` themes into `all`, and injecting the result into `theme1

These files will be explored in more detail further down, but it is helpful to know how they work together in order to understand the solution at a high level:

  • The first two files, default.ts and crypto.ts, define the default themes (light and dark) and Crypto themes (btc, eth and dot) respectively. The first file consists of 2 themes, whereas the crypto file consists of 3. Note that the amount of themes for each file does not matter — they will all be deep merged together.

Note that these files must adhere to the same object structure for deep merging to properly aggregate each property.

  • The third file, all.ts, imports both theme configurations and deep merges them into a singular object called AllThemes. The lodash.merge utility has been used here for the deep merge to happen. This is an aggregation step that combines all your theme configurations into one object.
  • The last file, theme.ts, imports the AllThemes object and adds theme properties to ThemeSet objects, that are provided by the styled-theming package. This underrated package allows you to import theme values (“header background color” for example) under your ThemeProvider component and automatically provide the correct value based on the currently active theme.

So the first 2 files define theme colours for every attribute you wish to theme — and there is no limit to how many of these files you define. File 3 deep merges all these files into one object, AllThemes. And finally AllThemes is imported and used in file 4 to create ThemeSet objects that are used in your UI components.

How ThemeSets are constructed

Now let’s work backwards to understand how this merging happens.

theme.ts defines each theme set. Let’s take backgroundColor:

export const backgroundColor: theme.ThemeSet = theme('mode', {
...AllThemes.background.primary,
});

Here we are providing this ThemeSet every property in our AllThemes.background.primary object. What this actually equates to is every single property of background.primary defined in our theme configuration files.

Here is the literal value:

export const backgroundColor: theme.ThemeSet = theme('mode', {
light: '#fafafa',
dark: '#0e0f11',
btc: '#fafafa',
eth: '#fafafa',
dot: '#fafafa'
});

Now let’s turn our attention to how AllThemes is actually constructed. Here is the merge statement that defines AllThemes:

// all.tsimport merge from 'lodash.merge';
import defaultThemes from './default';
import cryptoThemes from './crypto';
export const AllThemes = merge(
defaultThemes,
cryptoThemes,
// could merge more themes here...
);
export default AllThemes;

Lodash’s merge function accepts an arbitrary number of objects so you can merge as many theme files here simply by comma-separating them within merge()— a very useful function.

To add lodash.merge to your project, simply add the merge dependency, you don’t need to install the entire Lodash suite:

yarn add lodash.merge

Deep merging will combine all sub-properties of an object to another object without deleting any deep properties in the process.

Deep merging is in stark contrast to shallow merging, that usually pertains to overwriting properties after one child property. This means deeper properties that existed before a shallow merge will be deleted if they do not exist on the newly merged object.

The Literal Deep Merging Process

Referring to the background.primary property again, the merge function will combine the light and dark properties of the defaultThemes object with btc, dot and eth properties of the cryptoThemes object.

Examining this process literally, the defaultThemes primary background…

...
background: {
primary: {
light: '#fafafa',
dark: '#0e0f11',
}
...
}
...

… is being merged with the cryptoThemes primary background…

...
background: {
primary: {
btc: '#fafafa',
dot: '#f8f8f8',
eth: '#fcfcfc',
}
...
}
...

… which results in the merged AllThemes primary background:

...
background: {
primary: {
light: '#fafafa',
dark: '#0e0f11'
btc: '#fafafa',
dot: '#f8f8f8',
eth: '#fcfcfc',
}
...
}
...

This is the function that allows us to import an arbitrary number of theme files and ensure they are all deep merged, provided they adhere to the same structure.

Defining Themes

Let’s now look deeper into how themes are defined. Note that each theme configuration must adhere to a common structure in order for the deep merge to combine each configuration correctly — this structure is defined within each theme configuration file.

The following screenshot shows the default.ts file in its complete form. The defaultThemes object defines the general structure of the theme itself. Notice the v() function defined at the top — this will be discussed next:

Press enter or click to view image in full size
Default theme configuration for light and dark modes

Likewise, the crypto.ts file adheres to the exact same structure, with the exception that the v() function is injecting properties for 3 themes instead of 2 themes:

Press enter or click to view image in full size
Crypto theme configuration for BTC, DOT and ETH modes

Notice that the v() function allows us to define the exact same structure on each file and simply pass in values to v() to match the number of parameters the function takes — or in our case, the number of themes being defined. This has two benefits:

  • We can clearly see from v()’s signature how many themes need to be defined, along with their names.
  • We simply need to pass in v(…values) in the main configuration object without needing to define sub properties.

Therefore, if you are defining, say, 5 themes in a particular file, all you need to amend are:

  • The parameter names of v() and its returning properties.
  • The number of values passed into v() in the theme configuration object.

Putting it All Together

Now we have covered the implementation in detail, the reader can take what is useful and apply it to their theming setup.

What worked for my implementation is defining a theme/ folder in your project’s src/ directory that houses each theme configuration, your all.ts aggregation file, and your ThemeSet definitions:

# theme folder structure src/
theme/
all.ts
crypto.ts
default.ts
theme.ts
...

With this set up you can simply wrap your component tree with a <ThemeProvider /> component and import the necessary ThemeSets into your components.

If you create a new theme configuration file, just remember to import it into all.ts and add it to the merge() function. And that will be all the maintenance needed — nothing else touched.

For more details for setting up your theme components to accommodate this setup, check out my corresponding article: React Dark Mode with Styled Theming and Context.

In Summary

Deep merging plays an elegant yet crucial role in combining theme objects. The solution presented in this piece utilises React’s modular structure to easily import theme configurations and merge them into a single object, which in-turn can be imported again and used for your ThemeSet definitions.

I hope this has also shed some light on a viable deep merging use case in the realm of React applications.

--

--

Ross Bulat
Ross Bulat

Written by Ross Bulat

Programmer and Author. @ Parity Technologies, JKRB Investments

No responses yet