How to persist custom Material UI theme(Light & Dark) using Redux Toolkit and Local Storage in React

Mokshith Ajalade
9 min readMay 29, 2022

--

Image Source: Google

Hello there!! I have recently set up a light and dark theme using Material UI in my React project. I am writing this article in the hope that it might be of use to someone who is trying to use Material UI in their project and setting up custom themes with it.

What you will learn:

  • How to set up custom Material UI themes(light and dark)
  • How to toggle between light and dark theme
  • How to set up and use the Redux Tool kit
  • How to store and read data from local storage

In this article, I would assume,

  • You know basic React
  • Have a basic understanding of how Redux state management works
  • Already have an existing React app
  • Material UI version is v5.4.0

Our Goal

To persist(continue) the same theme as before even after the page is refreshed or opening the app after closing it.

What we will do to achieve our goal,

  1. Install Redux Toolkit, React-redux, and set up a global state for theme toggling
  2. Install Material UI and set up custom theme palettes for light and dark theme
  3. Use components from Material UI to build a basic UI
  4. Use local storage for persisting our theme

1. Install Redux Toolkit, React-Redux, and set up a global state for theme toggling

Install Redux Toolkit and React Redux

# If you use npm:
npm install @reduxjs/toolkit react-redux
# Or if you use Yarn:
yarn add @reduxjs/toolkit react-redux

You should also make sure that you have the React and Redux DevTools extensions installed in your browser:

Now that we have packages and extensions related to Redux in our project, it’s time to create a global “theme” state. We can do this in three steps,

  1. Create a Redux Store and provide it to React
  2. Create a State Slice and add it to the Redux Store
  3. Use Redux State and Actions in the UI to toggle our state(dispatch actions)

Create a Redux Store,

Create a file named src/store/store.js. Import the configureStore API from Redux Toolkit. We'll start by creating an empty Redux store, and exporting it:

Provide the Redux Store to React,

Once the store is created, we can make it available to our React components by putting a React-Redux <Provider> around our application in src/index.js. Import the Redux store we just created, put a <Provider> around your <App>, and pass the store as a prop:

Create a Redux State Slice,

Note: A “slice” is a collection of Redux reducer logic and actions for a single feature in your app, typically defined together in a single file. The name comes from splitting up the root Redux state object into multiple “slices” of state.

Add a new file named src/store/reducers/themeSlice.js. In that file, import the createSlice API from Redux Toolkit.

Creating a slice requires a string name to identify the slice, an initial state value, and one or more reducer functions to define how the state can be updated. Once a slice is created, we can export the generated Redux action creators and the reducer function for the whole slice.

Redux requires that we write all state updates immutably, by making copies of data and updating the copies. However, Redux Toolkit’s createSlice and createReducer APIs use Immer inside to allow us to write "mutating" update logic that becomes correct immutable updates.

We set our theme global state’s initial value to darkMode: false and write a reducer function to toggle our current theme mode when the action is dispatched.

Add Slice Reducers to the Store,

Next, we need to import the reducer function from the themeSlice and add it to our store. By defining a field inside the reducer parameter, we tell the store to use this slice reducer function to handle all updates to that state.

Use Redux State and Actions in React Components

Now we can use the React-Redux hooks to let React components interact with the Redux store. We can read data from the store with useSelector, and dispatch actions using useDispatch. Create a src/components/square/Square.js file with a <Square> component inside, then import that component into App.js and render it inside of <App>.

Now our UI looks like this

Open up your browser’s DevTools. Then, choose the “Redux” tab in the DevTools, and click the “State” button in the upper-right toolbar. You should see something that looks like this:

On the right, we can see that our Redux store is starting with an app state theme that looks like this:

{
theme: {
darkMode: false
}
}

Now when we click the “Toggle Theme” button:

  • The corresponding Redux action will be dispatched to the store
  • The theme slice reducer will see the action and update its state
  • The <Square> component will see the new state value from the store and re-render itself with the new data

The below image has been uploaded after clicking on the “Toggle Theme” button.

We can see three important things here:

  • When we clicked the “Toggle Theme” button, an action with a type of "theme/toggleTheme" was dispatched to the store
  • When that action was dispatched, the state.theme.darkMode field changed from false to true
  • And our UI got re-rendered after the global state changed, from Light Theme to Dark Theme

With this, we can confirm that the Redux “theme” global state is working as expected and we can also see that Redux Toolkit is generating “types” for us, so we don’t have to manually create them. Less boilerplate code for us:-)

With that, we can conclude creating a global state “theme”.

2. Install Material UI and set up custom theme palettes for light and dark theme

Install Material UI,

// with npm
npm install @mui/material @emotion/react @emotion/styled

// with yarn
yarn add @mui/material @emotion/react @emotion/styled

Set up a custom theme palette using Material UI,

To use custom palettes for light and dark modes, we can create a function that will return the correct palette depending on the selected mode, as shown here:

We can see on the above code snippet that there are different colors used based on whether the mode is light or dark. The next step is to use this function when creating the theme.

If we wish to customize the theme, we need to use the ThemeProvider component to inject a theme into our application. We can use the createTheme API from Material UI to create a custom theme based on the options received and pass it as a prop to ThemeProvider.

In the above code, we are setting the mode(local state) according to the darkMode(global theme state) and passing it as an argument to the getDesignTokens function. getDesignTokens function will return us a theme palette based on the mode parameter that it is receiving. And we will pass this theme object as a prop to the ThemeProvider component.

Note: Make sure ThemeProvider is a parent of the components you are trying to customize.

3. Use components from Material UI to build a basic UI

Let’s use an Icon provided by Material UI in our UI, to use it we need to install Material Icons.

To install,

# npm
npm install @mui/icons-material
#yarn
yarn add @mui/icons-material

We can access the theme variables inside our React components using useTheme hook provided by Material UI. Our code for UI will look like this,

Note: Here I have used both the global theme state from the Redux store and the theme object provided by ThemeProvider to show that we can access our current theme mode using any of the two ways.

This is what our current UI looks like,

Awesome!! We can successfully toggle between our custom light and dark themes. But now we have one last problem. As you might have observed before refreshing in the end, our app had dark theme => darkMode: true but after refreshing the page, it went back to the light theme. That’s because our app does not persist the theme state. Since the app reloads, the memory is cleared and hence the object is reset to the initial value. If you remember, we set our initial state value in our themeSlice as darkMode: false indicating our initial theme mode as light mode.

4. Use local storage for persisting our theme

We will be using local storage to solve the exact problem which we faced in the previous section, i.e., to load the UI with the right theme mode after a refresh or when opening the app after closing it. Mind you, our app was working as expected when toggling the theme after the initial load. The problem was that it did not load with the right theme after the refresh.

Since we are getting our initial state from a plain JS object it always remains the same, but what if we could get our initial state value from a source that is already keeping track of theme mode changes and doesn’t get cleared after a refresh or after closing the app? Enter the local storage !! The localStorage object allows you to save key/value pairs in the browser and doesn’t have an expiry date.

So now we need to do two things,
i. Load our global theme state’s initial state from local storage

ii. Change the value in local storage every time there is a change in theme mode

We are can load our initial state like this,

const initialState = {    darkMode: !!JSON.parse(localStorage.getItem("darkMode")),};

During the very first time our app loads onto the browser we have not set anything in our local storage. So when we try to read the value of the key darkMode we will get a null value. To convert it to false a Boolean value, we use !!.

Secondly, we can toggle the value in local storage every time the theme mode changes like this

const isDarkMode = !!JSON.parse(localStorage.getItem("darkMode"));localStorage.setItem("darkMode", !isDarkMode);

According to the rules of the reducers defined by Redux,

  • Reducers should only calculate the new state value based on the state and action arguments
  • Reducers are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
  • Reducers must not do any asynchronous logic or other “side effects”

So according to the last rule, toggling the local storage must be done outside of the reducer function as it is a side effect. So we use a thunk function for this operation.

A thunk is a specific kind of Redux function that can contain asynchronous logic. Thunks are written using two functions:

  • An inside thunk function, which gets dispatch and getState as arguments
  • The outside creator function, which creates and returns the thunk function

So we will create a thunk action creator in themeSlice and export it:

We can use a thunk function in the same way we use a typical Redux action creator:

store.dispatch(toggleTheme())

However, using thunks requires that the redux-thunk middleware (a type of plugin for Redux) be added to the Redux store when it's created. Fortunately, Redux Toolkit's configureStore function already sets that up for us automatically, so we can go ahead and use thunks here.

Here is the updated themeSlice.js

Now, all we need to do is dispatch our thunk function whenever there is a change in the theme mode. And to do this we need to import and use asyncToggleTheme instead of toggleTheme from themeSlice in Square.js.

Voila!! Now our React app is persisting the theme even after refreshing.

Please feel free to comment down below with your thoughts or any doubts you may have. Also please don’t forget to clap for me if this article added some value to you or if you have learned something new. This will encourage me to write more blogs in the future. Thank You!!

Additionally, you can look at the below links for more understanding of the tools we have used in this article.

Links to the code,

--

--