你太强了,组件已经搞得很漂亮了 👏!你现在遇到的问题,其实是 经典的 Next.js App Router hydration mismatch 闪烁问题,尤其当 UI 是根据 useState(客户端)变量渲染时,就会在 SSR 初始渲染和 hydration 之间出现不一致,从而导致“闪一下”。


✅ 问题核心

你的 UI 是这样工作的:

  1. 服务器端渲染阶段(SSR):没有 useState 的值,只能先渲染一个默认 UI(比如 theme 是 "light")。
  2. 客户端 hydration 后useEffect 获取 localStorage.getItem("theme"),并设置 useState,再触发一次渲染。
  3. 于是:组件从默认主题 → 切换到真实主题 → 闪一下 ✅

✅ 解决方案:用 useEffect + useState 是不够早的,我们需要:

🔹方案一:等到主题就绪再渲染 UI(推荐)

你可以在主题状态初始化之前 先 return null,等拿到真实 theme 后再渲染。

tsx
CopyEdit
const [theme, setTheme] = useState<"light" | "dark" | "system" | null>(null);

useEffect(() => {
  const stored = localStorage.getItem("theme") as "light" | "dark" | "system" | null;
  setTheme(stored ?? "system");
}, []);

if (theme === null) return null; // 主题未初始化,不渲染

👉 缺点是组件会短暂消失(空白),优点是绝不闪动。


🔹方案二:读取 theme 时加 SSR fallback,防止 mismatch

tsx
CopyEdit
const isSSR = typeof window === "undefined";
const initialTheme = isSSR ? "light" : localStorage.getItem("theme") ?? "system";

然后把这个作为 useState 初始值:

tsx
CopyEdit
const [theme, setTheme] = useState<Theme>(initialTheme);

👉 但这方案并不能 100% 避免 mismatch,只是减少闪动。