2

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>;
}
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
anon_dcs3spp
  • 2,342
  • 2
  • 28
  • 62

2 Answers2

0

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......

anon_dcs3spp
  • 2,342
  • 2
  • 28
  • 62
0

You can take advantage of the new MUI CSS theme variables to keep your themes synchronized, including light/dark mode, and avoid having to create a custom context. This guide includes many of the steps you'll need to follow.

First, swizzle the ColorModeToggle and add the useColorScheme hook to notify MUI whenever Docusaurus toggles the color mode:

import { useColorScheme } from '@mui/material';
import React, { useEffect } from 'react';

export default function ColorModeToggleWrapper(props): {
  // Get the MUI hook
  const { setMode } = useColorScheme();

  // Extract the docusaurus theme from the component properties
  const { value } = props;

  // Whenever the theme changes in docusaurus, trigger the change in MUI
  useEffect(() => {
    setMode(value);
  }, [value]);

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

Then, swizzle the Root component and wrap its children in a CssVarsProvider component:

import { experimental_extendTheme as extendTheme, Experimental_CssVarsProvider as CssVarsProvider, getInitColorSchemeScript } from '@mui/material';
import React from 'react';

const theme = extendTheme({
  colorSchemes: {
    light: {
      palette: {
        primary: {
          main: '#ff5252',
        },
        // ...
      },
    },
    dark: {
      palette: {
        primary: {
          main: '#52ffff',
        },
        // ...
      },
    },
  },
  // ...other properties
});

function Root({ children }) {
  return (
    <>
      {getInitColorSchemeScript()}
      <CssVarsProvider theme={theme}>{children}</CssVarsProvider>
    </>
  );
}

export default Root;

A few notes:

tgordon18
  • 1,562
  • 1
  • 19
  • 31