Nextjs 13 Hydration 失败,因为初始 UI 与服务器上呈现的内容不匹配

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

我正在使用下一个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.

React 文档错误链接

我不明白为什么会收到此错误,因为我正在访问

localStorage
内的
useEffect
,所以我希望服务器上生成的 HTML 在第一次渲染之前与客户端相同。

如何解决这个错误?

javascript reactjs next.js local-storage
5个回答
10
投票

对于 Next.js 13,一旦组件返回 jsx

mounted


function Component() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <></>;
  // write rest of your code
}


3
投票

我已经制定了一个解决方法,暂时解决了这个问题,但代价是放弃 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 响应


3
投票

当我将此 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>
  );
}

1
投票

我只是通过动态导入 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;

0
投票

对于 ShadCn,

ThemeProvider
标签应位于
html
body
标签下

© www.soinside.com 2019 - 2024. All rights reserved.