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

localStorage未定义与暗黑模式主题无法保存的技术求助

主题模式切换与本地存储问题排查及解决

问题现象

  • 移除typeof window !== 'undefined'判断后:主题刷新能正常保存,但会抛出localStorage未定义错误
  • 添加该判断后:主题无法保存,刷新页面后始终默认显示暗黑模式
  • 额外异常:暗黑模式下刷新页面,首次点击切换按钮即可生效;亮色模式下刷新页面,需要点击两次按钮才能切换主题

原问题代码

逻辑代码

var state = '';
if (typeof window !== 'undefined') {
  state = localStorage.getItem('darkMode');
}
const [darkMode, setDarkMode] = useState(state || 'dark');
const toggleMode = () => {
  setDarkMode(darkMode == 'dark' ? 'light' : 'dark');
};
useEffect(() => {
  localStorage.setItem('darkMode', darkMode);
}, [darkMode]);

按钮组件代码

<BsFillMoonStarsFill onClick={toggleMode} className="cursor-pointer text-2xl dark:hidden" />
<BsSunFill onClick={toggleMode} className="hidden cursor-pointer text-2xl dark:text-white dark:inline" />

问题根源

  1. 初始值获取逻辑失效:useState的初始值仅在组件首次挂载时执行一次。服务端渲染时window不存在,state为空字符串,因此初始值被强制设为'dark';客户端渲染时,即便localStorage有保存的主题值,也不会重新读取更新状态。
  2. 闭包陷阱导致切换异常:toggleMode函数中引用的darkMode是组件首次渲染时的状态快照。亮色模式下刷新后,初始darkMode仍为服务端渲染的'dark',第一次点击时实际是把'dark'切换为'light',但UI未同步最新状态,第二次点击才会基于更新后的状态值切换。
  3. 未做环境判断的存储操作:服务端渲染时执行useEffect中的localStorage.setItem会直接报错,因为服务端不存在window对象。

修复方案

1. 正确获取初始主题值

将localStorage的读取操作放到useEffect中,组件挂载后再执行,确保客户端能拿到正确的保存值:

const [darkMode, setDarkMode] = useState('dark'); // 先设置默认值

useEffect(() => {
  if (typeof window !== 'undefined') {
    const savedMode = localStorage.getItem('darkMode');
    if (savedMode) {
      setDarkMode(savedMode);
      // 同步DOM的dark类,确保样式和状态一致
      document.documentElement.classList.toggle('dark', savedMode === 'dark');
    }
  }
}, []); // 空依赖,仅在组件挂载时执行一次

2. 修复切换函数的闭包问题

使用setDarkMode的函数式更新方式,每次切换都基于最新的状态值,避免闭包陷阱:

const toggleMode = () => {
  setDarkMode(prevMode => prevMode === 'dark' ? 'light' : 'dark');
};

3. 给存储操作添加环境判断

在保存主题到localStorage时,同样判断window是否存在,避免服务端报错:

useEffect(() => {
  if (typeof window !== 'undefined') {
    localStorage.setItem('darkMode', darkMode);
    document.documentElement.classList.toggle('dark', darkMode === 'dark');
  }
}, [darkMode]);

完整修复后的代码

import { useState, useEffect } from 'react';
import { BsFillMoonStarsFill, BsSunFill } from 'react-icons/bs';

const ThemeToggle = () => {
  const [darkMode, setDarkMode] = useState('dark');

  // 组件挂载时读取本地存储的主题
  useEffect(() => {
    if (typeof window !== 'undefined') {
      const savedMode = localStorage.getItem('darkMode');
      if (savedMode) {
        setDarkMode(savedMode);
        document.documentElement.classList.toggle('dark', savedMode === 'dark');
      }
    }
  }, []);

  // 主题变化时保存到本地存储并同步DOM类
  useEffect(() => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('darkMode', darkMode);
      document.documentElement.classList.toggle('dark', darkMode === 'dark');
    }
  }, [darkMode]);

  const toggleMode = () => {
    setDarkMode(prev => prev === 'dark' ? 'light' : 'dark');
  };

  return (
    <>
      <BsFillMoonStarsFill 
        onClick={toggleMode} 
        className="cursor-pointer text-2xl dark:hidden" 
      />
      <BsSunFill 
        onClick={toggleMode} 
        className="hidden cursor-pointer text-2xl dark:text-white dark:inline" 
      />
    </>
  );
};

export default ThemeToggle;

额外说明

  • 新增document.documentElement.classList.toggle('dark')操作,确保主题状态和DOM的类名完全同步,避免样式显示异常
  • 函数式更新彻底解决了亮色模式下需要点击两次的问题,每次切换都基于最新状态
  • 所有涉及window和localStorage的操作都添加了环境判断,避免服务端渲染时的报错

内容的提问来源于stack exchange,提问作者Kevin Do

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 22:07:43