就像许多其他答案所说的那样,例如这个,Next.js 同时在客户端和服务器上运行,所以你需要一个守卫才能正确地从
localStorage
获取:
if (typeof localStorage !== "undefined") {
return localStorage.getItem("theme")
} else {
return "light"
}
这也是我正在做的事情,而且,由于我使用 DaisyUI 并在
<html data-theme={theme}>
上指定了我的主题,所以我基本上也用主题提供程序包装了我的整个应用程序。
这给我带来了两个问题:
localStorage
中的内容,并进入已保存的预期主题。如果我脱掉防护罩,那么一切实际上都会按预期工作,并且不会出现上述问题,但随后我在服务器上收到此错误:
⨯ src/lib/context/ThemeContext.tsx (37:10) @ getPreference
⨯ ReferenceError: localStorage is not defined
at getPreference
at ThemeProvider
36 | const storedPreference = localStorage.getItem("theme")
> 37 | return storedPreference
| ^
38 | ? stringToTheme(storedPreference)
39 | : "light"
我认为这意味着我可能需要以某种方式禁用整个应用程序的 SSR。这是要走的路吗?或者还有别的路可走吗?也许有一种方法可以仅为此禁用 SSR?
我已经尝试过类似的方法,它确实有效,尽管我不知道它是否理想,毕竟它禁用了 Next.js 本身最大的好处之一:
const DynamicApp = dynamic(
() =>
import("./dapp").then((mod) => mod.ThemedAndAuthedApp),
{
loading: () => (
<html>
<body>
<p>Loading...</p>
</body>
</html>
),
ssr: false,
}
)
我确实收到了这个错误:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
最好在保持服务器端渲染(SSR)功能的同时有效管理主题偏好。因为这就是NextJs的核心用法
首先使用默认主题初始化上下文提供程序中的主题状态,以避免客户端加载时可见的主题切换
// Initialize theme state with a safe default for SSR
const [theme, setTheme] = useState<Theme>(Theme.retro);
然后确保
getPreference
功能确保localStorage只能在客户端访问,防止服务器端错误。
// Function to read theme from localStorage
function getPreference(): Theme {
if (typeof window !== "undefined" && localStorage.getItem("theme")) {
return stringToTheme(localStorage.getItem("theme")!);
}
return theme; // Return current theme as fallback during SSR
}
最后使用
useEffect
钩子在组件挂载后应用主题,确保正确的水合并避免 UI 闪烁。
ThemeContext.tsx
:
import React, { createContext, useContext, useEffect, useState } from "react";
export enum Theme {
light = "light",
retro = "retro",
dark = "dark",
}
export function stringToTheme(s: string): Theme {
return Object.values(Theme).find(t => t === s) || Theme.light;
}
type ThemeContextType = {
theme: Theme;
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
cycleTheme: () => void;
syncTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(Theme.retro);
function getPreference(): Theme {
if (typeof window !== "undefined" && localStorage.getItem("theme")) {
return stringToTheme(localStorage.getItem("theme")!);
}
return theme;
}
function savePreference(theme: Theme): void {
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
}
function syncTheme(): void {
const savedTheme = getPreference();
setTheme(savedTheme);
document.documentElement.dataset.theme = savedTheme;
}
function cycleTheme(): void {
const themes = Object.values(Theme);
const currentThemeIndex = themes.indexOf(theme);
const nextTheme = themes[(currentThemeIndex + 1) % themes.length];
savePreference(nextTheme);
setTheme(nextTheme);
}
// Sync theme on client side after mount
useEffect(() => {
syncTheme();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ThemeContext.Provider value={{ theme, setTheme, cycleTheme, syncTheme }}>
{children}
</ThemeContext.Provider>
);
};
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("`useTheme` must be used within a `ThemeProvider`.");
}
return context;
}