You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用styled-components与React Context出现Hydration错误

主题切换服务端渲染不匹配问题解决

问题描述

应用支持深色(dark)和浅色(light)两种主题,当LocalStorage中的theme_key为light而非默认的dark时,出现错误:

Prop className did 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中,让服务端和客户端首次渲染主题一致:

  1. 修改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));
};
  1. 在页面的getServerSideProps中读取Cookie,传递初始主题:
export async function getServerSideProps(context) {
  const themeKey = context.req.cookies.theme_key || ThemeKeys.DARK;
  return {
    props: {
      initialThemeKey: themeKey,
    },
  };
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 02:35:48