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" />
问题根源
- 初始值获取逻辑失效:
useState的初始值仅在组件首次挂载时执行一次。服务端渲染时window不存在,state为空字符串,因此初始值被强制设为'dark';客户端渲染时,即便localStorage有保存的主题值,也不会重新读取更新状态。 - 闭包陷阱导致切换异常:
toggleMode函数中引用的darkMode是组件首次渲染时的状态快照。亮色模式下刷新后,初始darkMode仍为服务端渲染的'dark',第一次点击时实际是把'dark'切换为'light',但UI未同步最新状态,第二次点击才会基于更新后的状态值切换。 - 未做环境判断的存储操作:服务端渲染时执行
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
相关产品推荐
相关产品推荐

