Next.js中useDarkMode钩子实现的暗黑模式刷新后不持久化问题
问题
在Next.js应用中使用use-dark-mode钩子和@nextui-org/react组件开发主题切换组件,暗黑模式切换功能正常,但刷新页面后总会重置为浅色模式,即便之前已选择暗黑模式。
相关代码
import React from 'react'; import { Switch, Button } from '@nextui-org/react'; import { IoIosMoon } from 'react-icons/io'; import { FaSun } from 'react-icons/fa'; import useDarkMode from 'use-dark-mode'; interface ThemeToggleProps { isCompact: boolean; className?: string; // Optional className prop } export const ThemeToggle: React.FC<ThemeToggleProps> = ({ isCompact, className, }) => { const darkMode = useDarkMode(false); // Default to light mode return ( <div className={className}> {isCompact ? ( <Button isIconOnly onClick={darkMode.toggle} // Toggle between light and dark mode variant="light" > {darkMode.value ? <IoIosMoon size={24} /> : <FaSun size={24} />} </Button> ) : ( <Switch isSelected={darkMode.value} // Check if dark mode is enabled onChange={darkMode.toggle} // Toggle between light and dark mode size="lg" color="secondary" startContent={<IoIosMoon />} // Icon for dark mode endContent={<FaSun />} // Icon for light mode /> )} </div> ); };
现象
- 切换暗黑模式后,主题变更正常
- 刷新页面后,总会默认回到浅色模式
use-dark-mode钩子似乎无法跨页面刷新持久化主题设置
已尝试
- 原本预期
use-dark-mode会将状态存储在localStorage的默认dark-mode键下,但未成功持久化 - 怀疑与Next.js的SSR有关,或是初始渲染时忽略了localStorage的值
请问如何确保暗黑模式偏好跨页面刷新持久化?此问题是否因use-dark-mode不兼容Next.js SSR导致?最佳修复方案是什么?
解决方案
问题根源
确实是Next.js的SSR机制导致的问题:
- 服务端渲染时,
localStorage不存在,use-dark-mode无法读取存储的偏好设置,只能使用传入的默认值false(浅色模式) - 客户端 hydration 阶段,组件初始渲染仍沿用服务端的默认值,之后才会读取localStorage,但此时可能已触发主题重置,或NextUI主题未同步更新
use-dark-mode本身支持localStorage持久化,但在SSR场景下需要特殊处理初始值的获取逻辑。
修复步骤
1. 适配SSR的use-dark-mode初始值调整
不要直接传固定的false作为默认值,通过环境判断在客户端优先读取localStorage,服务端保持默认:
const darkMode = useDarkMode( typeof window !== 'undefined' ? localStorage.getItem('dark-mode') === 'true' : : false );
2. 用next-themes配合NextUI官方方案(推荐)
next-themes专门适配Next.js的SSR/SSG场景,能自动处理持久化、主题同步和hydration匹配问题,步骤如下:
- 安装依赖:
npm install next-themes
- 在
_app.tsx(App Router用layout.tsx)中配置主题提供者:
import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { NextUIProvider } from '@nextui-org/react'; export default function App({ Component, pageProps }) { return ( <NextThemesProvider defaultTheme="light" attribute="class"> <NextUIProvider> <Component {...pageProps} /> </NextUIProvider> </NextThemesProvider> ); }
- 修改
ThemeToggle组件使用useTheme钩子:
import React from 'react'; import { Switch, Button } from '@nextui-org/react'; import { IoIosMoon } from 'react-icons/io'; import { FaSun } from 'react-icons/fa'; import { useTheme } from 'next-themes'; interface ThemeToggleProps { isCompact: boolean; className?: string; } export const ThemeToggle: React.FC<ThemeToggleProps> = ({ isCompact, className, }) => { const { theme, setTheme } = useTheme(); const toggleTheme = () => { setTheme(theme === 'dark' ? 'light' : 'dark'); }; return ( <div className={className}> {isCompact ? ( <Button isIconOnly onClick={toggleTheme} variant="light" > {theme === 'dark' ? <IoIosMoon size={24} /> : <FaSun size={24} />} </Button> ) : ( <Switch isSelected={theme === 'dark'} onChange={toggleTheme} size="lg" color="secondary" startContent={<IoIosMoon />} endContent={<FaSun />} /> )} </div> ); };
3. 挂载后同步状态(兼容原有use-dark-mode)
如果坚持使用use-dark-mode,在组件挂载后手动同步localStorage状态,避免hydration不匹配:
import { useEffect } from 'react'; export const ThemeToggle: React.FC<ThemeToggleProps> = ({ isCompact, className, }) => { const darkMode = useDarkMode(false); useEffect(() => { const savedMode = localStorage.getItem('dark-mode') === 'true'; if (savedMode !== darkMode.value) { darkMode.set(savedMode); } }, []); // 其余代码不变 };
最佳方案
优先选择next-themes配合NextUI官方主题方案,它完全适配Next.js的服务端渲染逻辑,无需额外处理环境判断,代码更简洁且可靠性更高。
内容的提问来源于stack exchange,提问作者cryxnet
相关产品推荐
相关产品推荐

