Dark Mode

Building React App using Material UI with Support for Multiple Switchable Themes

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.

Written By
Praveen
Programmer | Tech Blogger | Multitasker | Learner Forever | Someone who believes knowledge grows by sharing | Looking forward to using spare time productively
See responses (9)