Building React App using Material UI with Support for Multiple Switchable Themes
Material UI is undoubtedly the best components library available out there for React. When I started using it, I found it challenging to get the theming right. After trying many approaches, I eventually found an approach that worked well for me. So, I decided to write an article on it.
In this article, I will demonstrate how to create a React application that supports multiple themes using Material UI and TypeScript. Let’s get started!
Also Read: Developing Websites with Multiple Themes using CSS Variables
Setting up the Project
The usual drill, create a TypeScript based React application using create-react-app
and add Material UI as a dependency.
npx create-react-app material-ui-multiple-themes --template typescript
cd material-ui-multiple-themes
npm install --save @material-ui/core
Creating Multiple Themes
Create a new folder named themes
under the src
directory. This is where we will keep all the Material UI theme objects. For our example, we will create two themes named – darkTheme and lightTheme. Like so:
import { createMuiTheme } from '@material-ui/core';
export const lightTheme = createMuiTheme({
palette: {
type: 'light',
primary: {
main: '#4167B2'
}
}
});
import { createMuiTheme } from "@material-ui/core";
export const darkTheme = createMuiTheme({
palette: {
type: "dark",
primary: {
main: "#000"
}
}
});
Now that we have multiple theme objects created, we need to create a function that will return a theme object given a theme name.
import { Theme } from "@material-ui/core";
import { lightTheme } from "./light";
import { darkTheme } from "./dark";
export function getThemeByName(theme: string): Theme {
return themeMap[theme];
}
const themeMap: { [key: string]: Theme } = {
lightTheme,
darkTheme
};
Creating a Custom Theme Provider
The ThemeProvider
component will have a state to store the selected theme name and a setter function to change the theme name. It will then retrieve the theme object using the getThemeByName
(that we created in the previous step) and the selected theme name(from the state). The retrieved theme object is then passed to MuiThemeProvider
.
import React, { useState } from 'react';
import { MuiThemeProvider } from "@material-ui/core";
import { getThemeByName } from './themes/base';
const ThemeProvider: React.FC = (props) => {
// State to hold the selected theme name
const [themeName, _setThemeName] = useState('lightTheme');
// Retrieve the theme object by theme name
const theme = getThemeByName(themeName);
return (
<MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>
);
}
export default ThemeProvider;
Now we need to expose our setter function to all the child components so that the theme can be changed from anywhere in the application. We can do exactly that by using React Context.
export const ThemeContext = React.createContext((themeName: string): void => {});
const ThemeProvider: React.FC = (props) => {
// Code removed for brevity
}
Now we will wrap MuiThemeProvider
inside the ThemeContext.Provider
giving it the setter function as the value so that any child component can access the setter function using ThemeContext.Consumer
or the useContext
hook.
return (
<ThemeContext.Provider value={_setThemeName}>
<MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>
</ThemeContext.Provider>
);
NOTE: You can even pass both selected theme name and the setter function in the same context if required.
To make the theme available to all the components, we will make ThemeProvider
as our root component in index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ThemeProvider from './ThemeProvider';
ReactDOM.render(
<ThemeProvider>
<App />
</ThemeProvider>,
document.getElementById("root")
);
Changing Theme from Nested Child Component
Now, we can use the ThemeContext
to retrieve the setter function in our child component and change the theme. Like so:
import React, { useContext } from 'react';
import { Button } from '@material-ui/core';
import { ThemeContext} from './ThemeProvider';
const App: React.FC = () => {
// Get the setter function from context
const setThemeName = useContext(ThemeContext)
return (
<div className="App">
<Button
variant="contained"
color="primary"
onClick={() => setThemeName("lightTheme")}
>
Set Light Theme
</Button>
<Button
variant="contained"
color="secondary"
onClick={() => setThemeName("darkTheme")}
>
Set Dark Theme
</Button>
</div>
);
}
export default App;
Fire up the application and try it out. You will be able to change the theme using the buttons.
Persisting the Theme Preference
You might have realized that hitting refresh will reset the selected theme to lightTheme
because we have hard-coded it in the ThemeProvider
component.
To solve this, we can either store the selected theme in the browser using localStorage or in the backend using APIs. To start off I will go ahead with localStorage as it’s much simpler.
Modify the ThemeProvider
component to read the theme name from localStorage instead of simply hard-coding lightTheme
as default.
// Read current theme from localStorage or maybe from an api
const curThemeName = localStorage.getItem("appTheme") || "lightTheme";
// State to hold the selected theme name
const [themeName, _setThemeName] = useState(curThemeName);
Similarly, whenever the user changes the theme we will store it in the localStorage. To do that we can wrap the setter function – that is why we prefixed the setter function with an underscore.
// Wrap _setThemeName to store new theme names in localStorage
const setThemeName = (themeName: string): void => {
localStorage.setItem("appTheme", themeName);
_setThemeName(themeName);
}
return (
<ThemeContext.Provider value={setThemeName}>
<MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>
</ThemeContext.Provider>
);
You can see the full implementation on material-ui-multiple-themes repository on GitHub.