Next.js 14中如何在Redux Toolkit初始状态使用localStorage?
解决Next.js + Redux Toolkit暗黑模式初始化的服务端报错与闪烁问题
核心思路
要解决这个问题,需要同时处理三个关键点:服务端无window对象的兼容性、避免客户端水化不匹配、消除页面加载时的主题闪烁。核心方案是提前在客户端渲染前设置主题DOM属性,同时让Redux状态兼容服务端/客户端环境。
步骤1:修改Redux切片,使用懒初始化兼容服务端
将初始状态改为懒初始化函数,区分服务端和客户端的取值逻辑,同时在reducer中同步更新localStorage:
"use client"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; export interface DarkModeState { value: boolean; }; // 懒初始化函数:服务端返回默认值,客户端读取localStorage或系统主题 const getInitialState = (): DarkModeState => { if (typeof window === "undefined") { // 服务端无法获取客户端状态,返回固定默认值(可根据需求调整) return { value: false }; } const storedValue = window.localStorage.getItem("darkMode"); // 无存储值时,优先匹配系统主题 if (storedValue === null) { const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; return { value: prefersDark }; } return { value: storedValue === "true" }; }; export const darkModeSlice = createSlice({ name: "darkMode", initialState: getInitialState, // 传入懒初始化函数 reducers: { setDarkMode: (state, action: PayloadAction<boolean>) => { state.value = action.payload; // 更新localStorage持久化状态 if (typeof window !== "undefined") { window.localStorage.setItem("darkMode", action.payload.toString()); } }, }, }); export const { setDarkMode } = darkModeSlice.actions; export default darkModeSlice.reducer;
步骤2:在根Layout中添加预渲染脚本,消除闪烁
在Next.js的根Layout(服务端组件)中添加内联脚本,在React hydration之前直接修改根元素的主题属性,确保页面初始渲染就是正确主题:
// app/layout.tsx import type { Metadata } from "next"; import { Inter } from "next/font/google"; import AppWrapper from "./AppWrapper"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Your App Title", description: "Your App Description", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <head> {/* 内联脚本:在React加载前设置主题 */} <script dangerouslySetInnerHTML={{ __html: ` (function() { const storedDarkMode = localStorage.getItem('darkMode'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = storedDarkMode !== null ? storedDarkMode === 'true' : prefersDark; // 直接给根元素设置主题属性 document.documentElement.setAttribute('data-theme-app', isDark ? 'dark' : 'light'); })(); `, }} /> </head> <body className={inter.className}> <AppWrapper>{children}</AppWrapper> </body> </html> ); }
步骤3:更新AppWrapper,同步Redux状态与UI
修改AppWrapper组件,确保Redux状态与实际主题保持同步,同时监听系统主题变化:
"use client"; import { useAppSelector, useAppDispatch } from "@/store/hooks"; import React, { useEffect } from "react"; import { setDarkMode } from "@/store/slices/darkModeSlice"; export default function AppWrapper({ children }: { children: React.ReactNode }) { const darkMode = useAppSelector((state) => state.darkMode.value); const dispatch = useAppDispatch(); useEffect(() => { // 组件挂载后,同步Redux状态与localStorage/系统主题 const storedValue = localStorage.getItem("darkMode"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; const isDark = storedValue !== null ? storedValue === "true" : prefersDark; if (darkMode !== isDark) { dispatch(setDarkMode(isDark)); } // 监听系统主题自动变化(仅当用户未手动设置过主题时生效) const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleThemeChange = (e: MediaQueryListEvent) => { if (localStorage.getItem("darkMode") === null) { dispatch(setDarkMode(e.matches)); } }; mediaQuery.addEventListener("change", handleThemeChange); return () => mediaQuery.removeEventListener("change", handleThemeChange); }, [dispatch, darkMode]); return ( <div id="app" data-theme-app={darkMode ? "dark" : "light"}> {children} </div> ); };
为什么这样能解决问题?
- 服务端兼容性:懒初始化函数在服务端返回固定默认值,避免
window未定义报错;客户端则读取localStorage或系统主题。 - 消除闪烁:根Layout的内联脚本在React渲染前执行,直接设置DOM主题属性,页面初始渲染就是正确样式,不会出现"闪白"或"闪黑"。
- 水化匹配:客户端Redux初始化时的状态与预渲染脚本设置的DOM属性一致,避免React hydration时的不匹配警告。
- 状态同步:
useEffect确保Redux状态与实际存储/系统主题保持一致,同时支持系统主题自动切换。
内容的提问来源于stack exchange,提问作者srdrce
相关产品推荐
相关产品推荐

