How to build a dark mode theme in React Material UI

Last updated on Oct 15, 2022 by Suraj Sharma



In this tutorial, you are going to learn how you can build a dark mode theme toggle button with React Hooks, TypeScript and Material UI

The source code is available on the Github repository, URL is available at the end of the article.



Getting started


I assume you have some basic knowledge about the material UI theme.

For this tutorial you will require to have Node >= 8.10 and npm >= 5.6 on your machine. To create a project, run:


npx create-react-app react-material-ui-dark-mode --template typescript

cd react-material-ui-dark-mode

npm start


Create the ThemeProvider Component


Go inside the src folder and create a theme.js file. However, it is not mandatory as material UI already has a default theme.


import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    type: 'light'
  }
})

export default theme;

This piece of code creates a default material UI theme that is available across your application using useTheme() custom react hook, for example see the code below


import { useTheme } from '@mui/material/styles';

const ReactMaterialComponent = () => {
  const defaultTheme = useTheme();
  ....

  return (
    <p>{defaultTheme.palette.type}}</p>
  );
}

export default ReactMaterialComponent;

There are many use cases where you would be require to access your application's theme inside your react components.

Second, create a React Context API ThemeDispatchContext.tsx file

Then, define an interface ThemeProviderProps to provide type to the ThemeProvider component.


  interface ThemeProviderProps {
    children: React.ReactNode
    theme: Theme
  }

Create a react Context using React.createContext() and a ThemeProvider function component thats returns a ThemeDispatchContext.Provider JSX element wrapped inside the Material UI ThemeProvider.


  const ThemeDispatchContext = React.createContext<any>(null)

  const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, theme }) => {

    return (
      <MuiThemeProvider theme={memoizedTheme}>
        <ThemeDispatchContext.Provider value={dispatch}>
          {children}
        </ThemeDispatchContext.Provider>
      </MuiThemeProvider>
    )
  }

The idea here is to create a reducer and pass it's dispatch to the value of ThemeDispatchContext.Provider so that it accessible across the children, later a toggle button can dispatch an action to toggle the theme.

So, based on the idea, create a reducer and pass it to the useReducer() hook


const themeInitialOptions = {
  paletteType: 'light'
}

const [themeOptions, dispatch] = React.useReducer((state: any, action: any)=> {
  switch (action.type) {
    case 'changeTheme':
      return {
        ...state,
        paletteType: action.payload
      }
    default:
      throw new Error();
  }
}, themeInitialOptions);

themeOptions state defines the current paletteType.

The piece of code below creates a memoized theme that would create a new theme object every time themeOptions.paletteType changes.


const memoizedTheme = React.useMemo(()=>{
  return createTheme({
    ...theme,
    palette: {
      type: themeOptions.paletteType
    }
  });
}, [themeOptions.paletteType]);

Final Code


The final code looks like this


import React from 'react';

import {
  createTheme,
  ThemeProvider as MuiThemeProvider,
  Theme } from '@mui/material/styles';

interface ThemeProviderProps {
  children: React.ReactNode
  theme: Theme
}

const ThemeDispatchContext = React.createContext<any>(null);

const ThemeProvider: React.FC<ThemeProviderProps> = ({
  children, theme
}) => {
  const themeInitialOptions = {
    paletteType: 'light'
  }

  const [themeOptions, dispatch] = React.useReducer(
    (state: any, action: any)=> {
    switch (action.type) {
      case 'changeTheme':
        return {
          ...state,
          paletteType: action.payload
        }
      default:
        throw new Error();
    }
  }, themeInitialOptions);

  const memoizedTheme = React.useMemo(()=>{
    return createTheme({
      ...theme,
      palette: {
        type: themeOptions.paletteType
      }
    }))
  }, [themeOptions]);

  return (
    <MuiThemeProvider theme={memoizedTheme}>
      <ThemeDispatchContext.Provider value={dispatch}>
        {children}
      </ThemeDispatchContext.Provider>
    </MuiThemeProvider>
  )
}

export default ThemeProvider;

A Bonus Code


As you might have noticed the above ThemeProvider is not reusable in the sense every time you import the ThemeProvider you will be required to import ThemeDispatchContext, for the same reason I have not exported the ThemeDispatchContext.

Instead, I am going to a create custom hook useChangeTheme() that does the heavy lifting of using ThemeDispatchContext, returns a changeTheme() that dispatches a 'changeTheme' action on calling.


export const useChangeTheme = () => {
  const dispatch = React.useContext(ThemeDispatchContext);
  const theme = useTheme();
  const changeTheme = React.useCallback(()=>
    dispatch({
      type: 'changeTheme',
      payload: theme.palette.type === 'light' ? 'dark' : 'light'
    }),
  [theme.palette.type, dispatch]);

  return changeTheme;
}


Finally to use it in your project you have to import ThemeProvider and changeTheme.

Here is the complete code with an example



Related Solutions


Rate this post


Suraj Sharma is a Full Stack Software Engineer. He holds a B.Tech degree in Computer Science & Engineering from NIT Rourkela.