NextJS App Router迁移后Material-UI useColorScheme报错求助
useColorScheme 必须在<CssVarsProvider />下调用的报错 问题描述
迁移到NextJS 13 App Router后,触发Material-UI报错:
Error: MUI:
useColorSchememust be called underreturn (<CssVarsProvider />)
该报错在<CssVarsProvider />外部调用useColorScheme时触发。但已在根layout.tsx中通过自定义ThemeProvider包裹所有页面,ThemeProvider为客户端组件(含'use client'标记),内部已初始化<CssVarsProvider />,但调用useColorScheme的UsesColorSchemeHook组件仍触发报错。
相关代码如下:
根layout.tsx
export default async function Layout() { return ( <html> <body> <ThemeProvider> <UsesColorSchemeHook /> </ThemeProvider> </body> </html> ); }
自定义ThemeProvider
'use client'; // ... export default function ThemeProvider({ children }) { // ... return ( <CacheProvider value={cache}> <CssVarsProvider theme={theme} modeStorageKey="color_mode"> <CssBaseline enableColorScheme /> {children} </CssVarsProvider> </CacheProvider> ); }
UsesColorSchemeHook组件
'use client'; // ... export default function UsesColorSchemeHook({ children }) { const { mode } = useColorScheme(); return <div>{mode}</div>; };
原因分析
NextJS App Router中,根layout.tsx默认是服务器组件。当它直接渲染客户端组件UsesColorSchemeHook时,NextJS会尝试提前对客户端组件进行流式拆分渲染,此时作为客户端组件的ThemeProvider还未完成初始化,导致UsesColorSchemeHook在<CssVarsProvider />上下文未就绪的情况下调用useColorScheme,触发报错。
解决方案
1. 确保客户端组件上下文层级正确
修改根layout.tsx,让ThemeProvider作为body的直接子元素,且所有依赖上下文的组件都严格嵌套在ThemeProvider内部:
export default async function Layout() { return ( <html> <body> <ThemeProvider> {/* 所有依赖MUI上下文的组件必须放在此处 */} <UsesColorSchemeHook /> </ThemeProvider> </body> </html> ); }
2. 给依赖上下文的组件添加就绪检查
在UsesColorSchemeHook中手动判断上下文是否就绪,避免提前调用useColorScheme:
'use client'; import { useColorScheme, ColorSchemeContext } from '@mui/material/styles'; import { useContext } from 'react'; export default function UsesColorSchemeHook({ children }) { const colorSchemeContext = useContext(ColorSchemeContext); // 上下文未就绪时返回加载态或空内容 if (!colorSchemeContext) return <div>Loading...</div>; const { mode } = useColorScheme(); return <div>{mode}</div>; };
3. 用客户端中间层统一包裹上下文
创建纯客户端组件作为上下文入口,隔离服务器组件与客户端上下文的渲染时序:
创建ClientLayout.js
'use client'; import ThemeProvider from './ThemeProvider'; export default function ClientLayout({ children }) { return ( <ThemeProvider> {children} </ThemeProvider> ); }
修改根layout.tsx
import ClientLayout from './ClientLayout'; export default async function Layout() { return ( <html> <body> <ClientLayout> <UsesColorSchemeHook /> </ClientLayout> </body> </html> ); }
4. 排查ThemeProvider内部的异步逻辑
如果ThemeProvider内部有异步加载主题的逻辑,需确保<CssVarsProvider />在异步操作完成后再渲染:
'use client'; import { useState, useEffect } from 'react'; import { CacheProvider } from '@emotion/react'; import { CssVarsProvider, CssBaseline } from '@mui/material/styles'; export default function ThemeProvider({ children }) { const [theme, setTheme] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const loadTheme = async () => { const fetchedTheme = await import('./theme'); setTheme(fetchedTheme.default); setIsLoading(false); }; loadTheme(); }, []); if (isLoading) return <div>Loading theme...</div>; return ( <CacheProvider value={cache}> <CssVarsProvider theme={theme} modeStorageKey="color_mode"> <CssBaseline enableColorScheme /> {children} </CssVarsProvider> </CacheProvider> ); }
内容的提问来源于stack exchange,提问作者Sergey Kolodyazhnyy

