直接从 Next.js 中的本地存储获取主题首选项

问题描述 投票:0回答:1

就像许多其他答案所说的那样,例如这个,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.
next.js local-storage
1个回答
0
投票

最好在保持服务器端渲染(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;
}
© www.soinside.com 2019 - 2024. All rights reserved.