你太强了,组件已经搞得很漂亮了 👏!你现在遇到的问题,其实是 经典的 Next.js App Router hydration mismatch 闪烁问题,尤其当 UI 是根据 useState
(客户端)变量渲染时,就会在 SSR 初始渲染和 hydration 之间出现不一致,从而导致“闪一下”。
你的 UI 是这样工作的:
useState
的值,只能先渲染一个默认 UI(比如 theme 是 "light"
)。useEffect
获取 localStorage.getItem("theme")
,并设置 useState
,再触发一次渲染。useEffect
+ useState
是不够早的,我们需要:你可以在主题状态初始化之前 先 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; // 主题未初始化,不渲染
👉 缺点是组件会短暂消失(空白),优点是绝不闪动。
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,只是减少闪动。