我正在使用下一个13.1.0。 我有一个 ContextProvider 设置浅色和深色主题
'use client';
import { Theme, ThemeContext } from '@store/theme';
import { ReactNode, useState, useEffect } from 'react';
interface ContextProviderProps {
children: ReactNode
}
const ContextProvider = ({ children }: ContextProviderProps) => {
const [theme, setTheme] = useState<Theme>('dark');
useEffect(() => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'light' || storedTheme === 'dark') {
setTheme(storedTheme);
} else {
localStorage.setItem('theme', theme);
}
// added to body because of overscroll-behavior
document.body.classList.add(theme);
return () => {
document.body.classList.remove(theme);
};
}, [theme]);
const toggle = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
};
export { ContextProvider };
我在我的根布局中使用它
import '@styles/globals.scss';
import { GlobalContent } from '@components/GlobalContent/GlobalContent';
import { ContextProvider } from '@components/ContextProvider/ContextProvider';
import { Inter } from '@next/font/google';
import { ReactNode } from 'react';
const inter = Inter({ subsets: ['latin'] });
interface RootLayoutProps {
children: ReactNode
}
const RootLayout = ({ children }: RootLayoutProps) => {
return (
<html lang="en" className={inter.className}>
<head />
<body>
<ContextProvider>
<GlobalContent>
{children}
</GlobalContent>
</ContextProvider>
</body>
</html>
);
};
export default RootLayout;
我在 GlobalContent 中使用主题值
'use client';
import styles from '@components/GlobalContent/GlobalContent.module.scss';
import { GlobalHeader } from '@components/GlobalHeader/GlobalHeader';
import { GlobalFooter } from '@components/GlobalFooter/GlobalFooter';
import { ThemeContext } from '@store/theme';
import { ReactNode, useContext } from 'react';
interface GlobalContentProps {
children: ReactNode
}
const GlobalContent = ({ children }: GlobalContentProps) => {
const { theme } = useContext(ThemeContext);
return (
<div className={`${theme === 'light' ? styles.lightTheme : styles.darkTheme}`}>
<GlobalHeader />
<div className={styles.globalWrapper}>
<main className={styles.childrenWrapper}>
{children}
</main>
<GlobalFooter />
</div>
</div>
);
};
export { GlobalContent };
我收到错误
Hydration failed because the initial UI does not match what was rendered on the server.
我不明白为什么会收到此错误,因为我正在访问
localStorage
内的 useEffect
,所以我希望服务器上生成的 HTML 在第一次渲染之前与客户端相同。
如何解决这个错误?
对于 Next.js 13,一旦组件返回 jsx
mounted
function Component() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return <></>;
// write rest of your code
}
我已经制定了一个解决方法,暂时解决了这个问题,但代价是放弃 SSR。
通过在我的 ContextProvider
上使用
动态导入,我禁用了服务器渲染,错误就消失了。作为奖励,从我的默认深色主题到保存在
localStorage
上的浅色主题的闪烁问题消失了。但我放弃了SSR的好处。如果有人找到更好的解决方案,请分享。
import '@styles/globals.scss';
import { GlobalContent } from '@components/GlobalContent/GlobalContent';
import { Inter } from '@next/font/google';
import dynamic from 'next/dynamic';
import { ReactNode } from 'react';
const inter = Inter({ subsets: ['latin'] });
interface RootLayoutProps {
children: ReactNode
}
// Fixes: Hydration failed because the initial UI does not match what was rendered on the server.
const DynamicContextProvider = dynamic(() => import('@components/ContextProvider/ContextProvider').then(mod => mod.ContextProvider), {
ssr: false
});
const RootLayout = ({ children }: RootLayoutProps) => {
return (
<html lang="en" className={inter.className}>
<head />
<body>
<DynamicContextProvider>
<GlobalContent>
{children}
</GlobalContent>
</DynamicContextProvider>
</body>
</html>
);
};
export default RootLayout;
此解决方案不会在站点范围内禁用 SSR。我使用以下代码添加了一个新的测试页面
async function getData() {
const res = await fetch('https://rickandmortyapi.com/api/character', { cache: 'no-store' });
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return (
<main>
{data.results.map((c: any) => {
return (
<p key={c.id}>{c.name}</p>
);
})}
</main>
);
}
运行后
npm run build
,可以看到测试页面使用的是ssr
在检查测试页面的响应时,我可以看到 HTML 响应
当我将此 Navbar 组件添加到 Next.js 13 中的layout.tsx 中的 RootLayout 中时,遇到了类似的问题。
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<Navbar />
<body className={font.className}>
{children}
</body>
</html>
);
}
我更改了上面的代码,而是在 body 标记内添加了导航栏,它解决了问题。
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={font.className}>
<Navbar />
{children}
</body>
</html>
);
}
我只是通过动态导入 ContextProvider 的默认导出来解决该错误,就像在 _app.tsx 中一样。我还在 localStorage 中保留上下文状态,并且它可以正常工作。
_app.tsx
import dynamic from "next/dynamic";
const TodoProvider = dynamic(
() => import("@/util/context").then((ctx) => ctx.default),
{
ssr: false,
}
);
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<TodoProvider>
<Component {...pageProps} />
</TodoProvider>
);
}
context.tsx
import React, {
useState,
FC,
createContext,
ReactNode,
useEffect,
} from "react";
export const TodoContext = createContext<TodoContextType | null>(null);
interface TodoProvider {
children: ReactNode;
}
const getInitialState = () => {
if (typeof window !== "undefined") {
const todos = localStorage.getItem("todos");
if (todos) {
return JSON.parse(todos);
} else {
return [];
}
}
};
const TodoProvider: FC<TodoProvider> = ({ children }) => {
const [todos, setTodos] = useState<ITodo[] | []>(getInitialState);
const saveTodo = (todo: ITodo) => {
const newTodo: ITodo = {
id: Math.random(),
title: todo.title,
description: todo.description,
status: false,
};
setTodos([...todos, newTodo]);
};
const updateTodo = (id: number) => {
todos.filter((todo: ITodo) => {
if (todo.id === id) {
todo.status = !todo.status;
setTodos([...todos]);
}
});
};
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("todos", JSON.stringify(todos));
}
}, [todos]);
return (
<TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>
{children}
</TodoContext.Provider>
);
};
export default TodoProvider;
对于 ShadCn,
ThemeProvider
标签应位于 html
和 body
标签下