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

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>
    );
};

为什么这样能解决问题?

  1. 服务端兼容性:懒初始化函数在服务端返回固定默认值,避免window未定义报错;客户端则读取localStorage或系统主题。
  2. 消除闪烁:根Layout的内联脚本在React渲染前执行,直接设置DOM主题属性,页面初始渲染就是正确样式,不会出现"闪白"或"闪黑"。
  3. 水化匹配:客户端Redux初始化时的状态与预渲染脚本设置的DOM属性一致,避免React hydration时的不匹配警告。
  4. 状态同步:useEffect确保Redux状态与实际存储/系统主题保持一致,同时支持系统主题自动切换。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 08:40:31