使用styled-components与React Context出现Hydration错误
主题切换服务端渲染不匹配问题解决
问题描述
应用支持深色(dark)和浅色(light)两种主题,当LocalStorage中的theme_key为light而非默认的dark时,出现错误:
Prop
classNamedid not match. Server:
原因是服务端先渲染深色主题,客户端读取LocalStorage后重新渲染浅色主题,导致服务端与客户端的HTML不匹配,触发Hydration错误。
相关代码
ThemeContext.ts
export const ThemeContext = createContext<{ theme: AppThemeInterface, setTheme: Dispatch<SetStateAction<AppThemeInterface>>, // eslint-disable-next-line no-unused-vars updateThemeKey: (newThemeKey: ThemeKeys) => void, }>({ theme: getTheme(ThemeKeys.DARK), // 仅包含接口定义的属性对象 setTheme: () => null, updateThemeKey: () => null, });
AppThemeProvider.ts
从LocalStorage获取theme_key,通过该key获取主题并注入到ThemeContext:
export const AppThemeProvider = ({ children }: AppThemeProviderProps) => { const [currentThemeKey, setCurrentThemeKey] = useLocalStorage<ThemeKeys>('theme_key', ThemeKeys.DARK); const [theme, setTheme] = useState<AppThemeInterface>(getTheme(currentThemeKey)); const updateThemeKey = (value: ThemeKeys) => { setCurrentThemeKey(value); }; return ( <ThemeContext.Provider value={ { theme, setTheme, updateThemeKey } }> <ThemeProvider theme={theme}> {children} </ThemeProvider> </ThemeContext.Provider> ); };
useLocalStorageHook
import { useState } from 'react'; export const useLocalStorage = <T>(key: string, initialValue: T) => { const [value, setValue] = useState<T>(() => { if (typeof window === 'undefined') { return initialValue; } const lcItem = localStorage.getItem(key); const endVal = lcItem ? JSON.parse(lcItem) : initialValue; localStorage.setItem(key, JSON.stringify(endVal)); return endVal; }); const saveValue = (value: T): void => { localStorage.setItem(key, JSON.stringify(value)); }; const onChangeValue = (value: T): void => { saveValue(value); setValue(value); }; return [value, onChangeValue] as const; };
解决方案
核心问题是服务端无法访问客户端LocalStorage,导致首次渲染主题不一致。以下是三种可行方案:
方案1:客户端挂载后延迟切换主题(最简单)
修改AppThemeProvider.ts,让服务端和客户端首次都用默认主题,客户端挂载后再读取LocalStorage更新主题,避免Hydration不匹配:
export const AppThemeProvider = ({ children }: AppThemeProviderProps) => { // 服务端与客户端首次渲染统一用默认dark const [currentThemeKey, setCurrentThemeKey] = useState<ThemeKeys>(ThemeKeys.DARK); const [theme, setTheme] = useState<AppThemeInterface>(getTheme(ThemeKeys.DARK)); useEffect(() => { // 客户端挂载后读取LocalStorage并更新主题 const savedKey = localStorage.getItem('theme_key') as ThemeKeys || ThemeKeys.DARK; setCurrentThemeKey(savedKey); setTheme(getTheme(savedKey)); localStorage.setItem('theme_key', savedKey); }, []); const updateThemeKey = (value: ThemeKeys) => { localStorage.setItem('theme_key', value); setCurrentThemeKey(value); setTheme(getTheme(value)); }; return ( <ThemeContext.Provider value={{ theme, setTheme, updateThemeKey }}> <ThemeProvider theme={theme}> {children} </ThemeProvider> </ThemeContext.Provider> ); };
缺点是会有一瞬间的主题闪烁,但不会触发Hydration错误。
方案2:通过Cookie传递主题偏好(无闪烁)
因为服务端可以读取Cookie,所以把主题偏好存在Cookie中,让服务端和客户端首次渲染主题一致:
- 修改
updateThemeKey,同时更新LocalStorage和Cookie:
const updateThemeKey = (value: ThemeKeys) => { localStorage.setItem('theme_key', value); // 设置Cookie,有效期1年 document.cookie = `theme_key=${value}; max-age=31536000; path=/`; setCurrentThemeKey(value); setTheme(getTheme(value)); };
- 在页面的
getServerSideProps中读取Cookie,传递初始主题:
export async function getServerSideProps(context) { const themeKey = context.req.cookies.theme_key || ThemeKeys.DARK; return { props: { initialThemeKey: themeKey, }, }; }
- 修改
AppThemeProvider接收初始主题props:
export const AppThemeProvider = ({ children, initialThemeKey }: AppThemeProviderProps & { initialThemeKey: ThemeKeys }) => { const [currentThemeKey, setCurrentThemeKey] = useState<ThemeKeys>(initialThemeKey); const [theme, setTheme] = useState<AppThemeInterface>(getTheme(initialThemeKey)); const updateThemeKey = (value: ThemeKeys) => { localStorage.setItem('theme_key', value); document.cookie = `theme_key=${value}; max-age=31536000; path=/`; setCurrentThemeKey(value); setTheme(getTheme(value)); }; return ( <ThemeContext.Provider value={{ theme, setTheme, updateThemeKey }}> <ThemeProvider theme={theme}> {children} </ThemeProvider> </ThemeContext.Provider> ); };
此方案完全避免闪烁和Hydration错误,体验最佳。
方案3:局部禁用Hydration检查
如果仅个别组件出现问题,可以给组件添加suppressHydrationWarning属性,忽略该组件的匹配警告:
<div suppressHydrationWarning>{/* 主题相关内容 */}</div>
注意:不推荐全局使用,仅适合局部场景。
内容的提问来源于stack exchange,提问作者DF1
相关产品推荐
相关产品推荐

