How to persist custom Material UI theme using Redux Toolkit and Local Storage in React
--
- By Mokshith Ajalade, CoffeeBeans Consulting
Hello there!! I have recently set up a light and dark theme using Material UI in my React project. I am writing this blog with the hope that it will be useful 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,
- Install Redux Toolkit, React-redux, and set up a global state for theme toggling
- Install Material UI and set up custom theme palettes for light and dark theme
- Use components from Material UI to build a basic UI
- 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-reduxOr
#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:
- React DevTools Extension:
- React DevTools Extension for Chrome
- React DevTools Extension for Firefox - Redux DevTools Extension:
- Redux DevTools Extension for Chrome
- Redux DevTools Extension for Firefox
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,
- Create a Redux Store and provide it to React
- Create a State Slice and add it to the Redux Store
- 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:
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 fromfalse
totrue
- 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 by 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 themeobject
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 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
andaction
arguments - Reducers are not allowed to modify the existing
state
. Instead, they must make immutable updates, by copying the existingstate
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
andgetState
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.