Search code examples
material-uidocusaurus

How to synchronise dark/light mode between Docusaurus and MaterialUI?


I am new Docusaurus user, trying to synchronise Docusaurus dark/light mode with MaterialUI's dark/light mode. For example, when the toggle switch is changed from light to dark mode in Docusaurus then dark mode should be activated in MaterialUI.

My approach so far has been to swizzle Docusaurus ColorModeToggle via wrapping. From the wrapped ColorModeToggle I retrieve a function stored in a React context to toggle the light/dark theme in MaterialUI. Within the swizzled Root I use the react context provider which, in turn, wraps a MaterialUI ThemeProvider. For further details I have included the code below.

However, when I browse my site, I get the following error:

Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

Has anyone else managed to synchronise Docusaurus light/dark theme with MaterialUI?

The swizzled ColorModeToggle

import React from "react";
import ColorModeToggle from "@theme-original/ColorModeToggle";
import { useToggleTheme } from "@site/src/components/MuiTheme";

export default function ColorModeToggleWrapper(props) {
  console.log("<ColorModeToggleWrapper> properties = " + JSON.stringify(props));

  // "value" holds docusaurus color theme. Either "light" or "dark"
  const { value } = props;
  const muiToggle = useToggleTheme();

  console.log("Docusaurus theme = " + value);
  console.log("MUI theme dark = " + muiToggle());

  return (
    <>
      <ColorModeToggle {...props} />
    </>
  );
}

The React Context

import React, { useContext } from "react";
import { createTheme, ThemeProvider } from "@mui/material/styles";

const CustomThemeContext = React.createContext({ toggleTheme: () => {} });

const darkTheme = createTheme({
  components: {
    MuiListItemText: {
      styleOverrides: {
        primary: {
          color: "orange",
        },
        secondary: {
          color: "purple",
        },
      },
    },
  },
  palette: {
    mode: "dark",
    primary: {
      main: "hsl(8,71%,28%)" /* burgundy mapped to link */,
    },
    secondary: {
      main: "hsl(61,78%,26%)" /* brown */,
    },
  },
});

const lightTheme = createTheme({
  components: {
    MuiListItemText: {
      styleOverrides: {
        primary: {
          color: "aqua",
        },
        secondary: {
          color: "grey",
        },
      },
    },
  },
  palette: {
    mode: "light",
    primary: {
      main: "hsl(8,10%,18%)" /* burgundy mapped to link */,
    },
    secondary: {
      main: "hsl(61,18%,26%)" /* brown */,
    },
  },
});

export function CustomThemeProvider({ children }) {
  const [dark, setDark] = React.useState(false);

  function toggleTheme() {
    console.log("toggleTheme :: from dark = " + dark);
    if (dark === true) {
      setDark(false);
    } else {
      setDark(true);
    }
  }

  const theme = React.useMemo(() => {
    if (dark === true) {
      return createTheme(darkTheme);
    }
    return createTheme(lightTheme);
  }, [dark]);

  return (
    <CustomThemeContext.Provider value={toggleTheme}>
      <ThemeProvider theme={theme}>{children}</ThemeProvider>
    </CustomThemeContext.Provider>
  );
}

export function useToggleTheme() {
  const context = useContext(CustomThemeContext);
  if (context === undefined) {
    throw new Error(
      "useCustomThemeContext must be used within an CustomThemeProvider"
    );
  }
  return context;
}

The Root component

import React from "react";
import { CustomThemeProvider } from "@site/src/components/MuiTheme";
import App from "@site/src/components/App";

export default function Root({ children }) {
  return (
    <>
      <CustomThemeProvider>
        <App children={children}></App>
      </CustomThemeProvider>
    </>
  );
}

The App component

import React from "react";

export default function App(props) {
  return <React.Fragment>{props.children}</React.Fragment>;
}

Solution

  • Solved why the error message was happening. I was calling the toggleTheme function directly during rendering. Further details are explained here

    I updated the ColorModeToggleWrapper to be as listed below. This uses useEffect to monitor change of the Docusaurus value property. When the value changes then useEffect toggles the Material-UI theme using a reference to the function stored in the react context.

    import React, { useEffect } from "react";
    import ColorModeToggle from "@theme-original/ColorModeToggle";
    import { useToggleTheme } from "@site/src/components/MuiTheme";
    
    export default function ColorModeToggleWrapper(props) {
      // extract the docusaurus theme from the component properties
      const { value } = props;
    
      // get the toggleTheme function from the context
      const toggleTheme = useToggleTheme();
    
      // whenever the theme changes in docusaurus trigger the change
      // in MaterialUI by calling the callback function stored in the react
      // context
      useEffect(() => {
        console.log("Docusaurus theme changed to = " + value);
        console.log("Synching theme change with MaterialUI");
    
        toggleTheme();
      }, [value]);
    
      return (
        <>
          <ColorModeToggle {...props} />
        </>
      );
    }
    

    This solved the error detailed in the question. Now I just need to ensure that the initial starting state of the Material-UI theme matches the initial state of the Docusaurus theme......