React Dark Mode with Styled Theming and Context

How to toggle themes with React Hooks, context and styled components

Bring dark mode to your React apps

Dark mode is becoming more commonly supported in apps, both on the web and natively. React is in a great position to support such a feature, by leveraging the framework’s capabilities and packages readily available in the surrounding ecosystem. Toggling between a dark and light mode can be achieved very elegantly and modularly, as will be demonstrated here.

This article will walk through the entire process of setting up a dark mode for your app, and a means to toggle between a light and dark theme. The demo I designed for this talk is freely available on Github to aid in the reader’s understanding.

To achieve a dark mode and toggle function, the following tools will be used:

React’s Context API. We will be defining a context specifically for managing theme toggling, that will be accessible anywhere in your app

The useContext hook will be used specifically for getting our theme toggle() function from the above context. useContext gets the value of a provided context from anywhere in your component tree

The useState hook to persist what mode the app is currently set to: either light or dark

styled-components and styled-theming packages. Styled components allow us to write CSS directly within a component, and with that come a range of benefits for managing styles throughout your app. The latter package, Styled Theming, builds on top of Styled Component’s ThemeProvider context, and provides some handy tools for managing themes that can scale

Note: If you have not yet delved into these packages, I wrote an introduction article to get fellow developers up to speed.

Setting Up the Project

To set up the project on your machine, either clone the Github repository or generate a new Create React App project with the required packages:

With the project initiated we are ready to delve into our theming solution.

Recapping <ThemeProvider />

An elegant theming solution must be scalable, and the configuration of which must be accessible throughout your entire app — this is where React’s Context API comes into play, allowing us to access a context value from anywhere in our component tree. A React theming solution therefore is heavily reliant on Context.

styled-components actually provides us with a context provider specifically for theming, in the form of <ThemeProvider />. To use this component, we simply have to wrap it around the <App /> component, or your root component:

With styled-theming, instead of passing theme properties into the theme prop, such as a backgroundColor or textColor, we instead define the theme we are working with. In this case we have a mode theme, but you are not limited to just a one-dimensional theme, as my previous article delves into.

From here, any component within <ThemeProvider/> can now access the context’s theme prop. styled-theming makes this super simple with the theme() utility function, where we can define styles for each component based on mode.

As well as the theme() utility, we can also get the context value using styled-component’s higher-order component, withTheme(). withTheme() provides the wrapped component with a theme prop, being the value we defined in our context:

Beyond style properties, other assets will most likely be based on your theme, such as artwork or text labels. E.g. a theme toggle button maybe display “Switch to Dark Mode” on light mode, and “Switch to Light Mode” on dark mode. Therefore it is critical to also obtain our theme configuration from within our functions, as props. withTheme() allows us to do exactly this.

Now, with that brief refresher on ThemeProvider, let’s explore how we can expand on this concept and add another context for toggling our theme.

Defining our own Theme Context

<ThemeProvider /> is great; it provides a solution of giving our entire app our theme configuration without having to even think about context. However, we still need a means to be able to switch from light to dark, and vice-versa.

Theme toggling needs context

It would be very useful, arguably crucial, to have the ability to toggle a theme from anywhere in the app. Perhaps there will be a switch in your app settings, another one at the top of the home page, or perhaps in a modal — heck, even in an automated demo showcasing what your app is capable of.

The bottom line is that our theme’s toggle() function must be accessible anywhere in the app. In order for this to be possible, we need to create its own context.

Why not just use <ThemeProvider />?

You may be wondering at this point why we do not embed a toggle() function inside our theme prop of <ThemeProvider />, after all, it would provide all child components access to the function.

Doing so would break the conventions of styled-theming, that expects strings to be passed into the theme() utility function. It would also be confusing for other developers to embed functions around our theme configuration. For this reason, I have opted to keep the functions and theme configuration in separate contexts.

What implications does this have on our app? Well, we just need to introduce another context for our toggle() function:

We now have another context, <ThemeToggleContext />, that wraps around <ThemeProvider />.

In reality though, this looks a bit messy — what would be much neater is if we could actually combine these two contexts into a unified component, that handles all our context logic — and wrap that component around <App />. This is exactly what we will do.

Concretely, we can combine both these contexts in another component, and wrap that component around <App />.

This would make our code much more readable, and manageable when it comes to expanding or amending contexts. Let’s create a new file named ThemeContext.js, and from it export a component, MyThemeProvider, that will handle contexts for both our theming configuration and our theme toggling function.

Implementing ThemeContext.js

Define an exportable useContext object, allowing us to getThemeToggleContext’s value from any other component, just by importing this object.

Note:useContext is a built-in React hook that gets a value of a context. It is used with the following syntax:

const contextValue = () => React.useContext(MyContext);

We will essentially be using this hook for our ThemeToggleContext, and making it exportable to be used within any component as a quick means of obtaining the context value — aka, our toggle() function.

Define an exportable component, MyThemeProvider, that will include both contexts to wrap around <App />.

Defining the Toggle Context

Straight away, we can import the necessary components into ThemeContext.js, and define our second context for toggle(). This context is defined as ThemeToggleContext:

Before delving into the implementation of MyThemeProvider, let’s review what was defined here.

ThemeToggleContext has been defined — the context object that will handle our theme management functions. It has been given a default value of an empty toggle() function.

A note on default context values

Does it matter that we have not defined our toggle() function from as the default value? No — because our ThemeToggleContext is the top most component in our tree, and therefore there will be no parent components needing that default value.

The React docs sum up the default value behaviour in one sentence: The defaultValue argument is only used when a component does not have a matching [context] Provider above it in the tree.

Furthermore, we have not planned to use this default value. It can be treated more like a signature function, so developers understand what the context is intended to consist of.

Alternatively, it can be left blank, simply being defined as React.createContext();. The context value will actually be defined in the JSX of MyThemeProvider, which we will explore next.

Exporting a useContext object

The second definition of ThemeContext.js is useTheme. useTheme is just a useContext hook, that can be imported into any component to obtain the value of the given context.

Now, within any component wrapped by the context provider, we can import useTheme and obtain the context value:

But in order for useTheme to work, we need to implement our MyThemeProvider component.

Combining the two contexts in MyThemeProvider

As mentioned earlier, the MyThemeProvider needs to combine both contexts in order for our full theming solution to work. Not only this, it can also include some initial page styling via a Wrapper styled component too.

The majority of the function is now developed, however, we are still missing the toggle() function implementation, and our <ThemeProvider/>mode is still hard-coded as light, and therefore it cannot be updated.

To alleviate these bottlenecks, the last piece of the puzzle is state.

Introducing state to manage theme mode

State needs to be introduced to manage the actual theme mode: light or dark, that will trigger a re-render upon the value changing. The useState hook can be used for this.

The hook can be added above toggle() with a default mode of light. Furthermore, toggle() can now be fully implemented:

And that concludes our theming setup! Our contexts are now hooked up and state is in charge of updating our <ThemeProvider />.

Calling toggle() with onClick

The last section in this talk will demonstrate how to call toggle() from a button click inside <App />.

To demonstrate styled-theming to a greater extent, I have defined two more properties in theme.js for button styling. In a light mode, the button will be dark grey to contrast with the white background, with white text. In a dark mode, the button will be a very light grey, with black text:

Notice here that the withTheme() HOC is also being utilised, provided by styled-components, to obtain the theme prop. This is then used to display button text, that depends on the current theme mode.

This button (or multiple buttons) can be embedded anywhere in your app; just import useTheme to get that toggle() function from the context provider.

In Summary

The final project resembles the following result:

This talk has taken you through a multi-context setup of theming your app, and a means to manipulate that theming through a context. To summarise:

We have revised the usage of styled-components and styled-theming to be used in conjunction with a dark and light mode for your app.

The useState and useContext hooks have been used to persist a theme mode and get the toggling context value respectively.

Global theme properties have been defined in a separate file, available to be imported into any other component.

The withThemeuseContext hook can be imported into any component to fetch our theme management functions.

Additional challenge: localStorage

As an additional challenge, incorporate localStorage to persist your theme changes through page refreshes. localStorage caches data indefinitely, records of which will only be removed if the user deletes website data, or if you programatically remove a localStorage item. Because localStorage items do not have time limits like session data does, it is ideal to save data like a theme mode, that is not often changed nor need to be expired due to security concerns.