如何实现Docusaurus与MaterialUI的明暗模式同步?
问题背景
作为Docusaurus新手,想要实现Docusaurus明暗开关切换时,MaterialUI主题同步切换的效果。当前采用的方案是:通过swizzle包装Docusaurus的ColorModeToggle组件,利用React Context获取MaterialUI的主题切换函数,并在swizzle后的Root组件中用Context Provider包裹MaterialUI的ThemeProvider。但运行时出现错误:
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.
原代码展示
包装后的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" 存储Docusaurus的主题值:"light" 或 "dark" const { value } = props; const muiToggle = useToggleTheme(); console.log("Docusaurus theme = " + value); console.log("MUI theme dark = " + muiToggle()); return ( <> <ColorModeToggle {...props} /> </> ); }
React Context(MuiTheme)
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; }
Root组件
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> </> ); }
App组件
import React from "react"; export default function App(props) { return <React.Fragment>{props.children}</React.Fragment>; }
错误原因分析
原代码的核心问题在于:ColorModeToggleWrapper组件每次渲染时都会直接调用muiToggle()(即toggleTheme函数),该函数会修改CustomThemeProvider中的dark状态,触发组件重渲染;重渲染后又会再次调用muiToggle(),形成无限更新循环,最终导致React抛出最大更新深度超限的错误。
修正方案
我们需要让MaterialUI的主题状态跟随Docusaurus的主题值同步变化,而非通过翻转函数手动切换,具体修改如下:
1. 重构React Context(MuiTheme)
修改Context,暴露主题状态和状态设置函数,而非仅提供翻转函数,同时初始化时匹配Docusaurus的初始主题:
import React, { useContext } from "react"; import { createTheme, ThemeProvider } from "@mui/material/styles"; const CustomThemeContext = React.createContext({ dark: false, setDark: () => {} }); // 保留原有的darkTheme和lightTheme定义 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); // 初始化时读取Docusaurus存储的主题,保持初始状态一致 React.useEffect(() => { const savedTheme = localStorage.getItem('theme') || 'light'; setDark(savedTheme === 'dark'); }, []); const theme = React.useMemo(() => { return dark ? createTheme(darkTheme) : createTheme(lightTheme); }, [dark]); return ( <CustomThemeContext.Provider value={{ dark, setDark }}> <ThemeProvider theme={theme}>{children}</ThemeProvider> </CustomThemeContext.Provider> ); } export function useThemeContext() { const context = useContext(CustomThemeContext); if (context === undefined) { throw new Error( "useThemeContext must be used within an CustomThemeProvider" ); } return context; }
2. 修正ColorModeToggleWrapper组件
使用useEffect监听Docusaurus主题值的变化,同步更新MaterialUI的主题状态:
import React, { useEffect } from "react"; import ColorModeToggle from "@theme-original/ColorModeToggle"; import { useThemeContext } from "@site/src/components/MuiTheme"; export default function ColorModeToggleWrapper(props) { const { value: docusaurusTheme } = props; const { setDark } = useThemeContext(); // 当Docusaurus主题变化时,同步设置MUI主题 useEffect(() => { setDark(docusaurusTheme === 'dark'); }, [docusaurusTheme, setDark]); return <ColorModeToggle {...props} />; }
3. Root与App组件保持不变
Root和App组件无需修改,继续使用原代码即可。
效果验证
修改完成后,切换Docusaurus的明暗开关时,MaterialUI的主题会自动同步切换,且不会再出现无限更新循环的错误。
内容的提问来源于stack exchange,提问作者anon_dcs3spp

