React: How to Scale Theme Sets with Deep Merging
Create sets of themes and inject them as one object into React theme providers
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:
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:
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.tsandcrypto.ts, define the default themes (lightanddark) and Crypto themes (btc,ethanddot) 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 calledAllThemes. Thelodash.mergeutility 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 theAllThemesobject and adds theme properties toThemeSetobjects, that are provided by thestyled-themingpackage. This underrated package allows you to import theme values (“header background color” for example) under yourThemeProvidercomponent 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.mergeDeep 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:
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:
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.
