【问题标题】:Nextjs 13 Hydration failed because the initial UI does not match what was rendered on the serverNextjs 13 Hydration 失败,因为初始 UI 与服务器上呈现的内容不匹配
【发布时间】:2023-02-19 15:13:47
【问题描述】:

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

我不明白为什么我会收到此错误,因为我正在我的useEffect 中访问localStorage,所以我希望服务器上生成的 HTML 在第一次呈现之前与客户端相同。

我该如何解决这个错误?

【问题讨论】:

  • 您是否分析过确切的 HTML 差异? (应该是错误信息的一部分)
  • 错误消息没有说明 HTML 差异。我开始认为这是 nextjs 13 的一个错误,因为 13 还没有准备好投入生产。错误消息也会随机出现。也许是 1/10 倍?
  • 我添加了一张图片,显示我在控制台中遇到的错误以及 React 文档错误链接。

标签: javascript reactjs next.js local-storage


【解决方案1】:

我已经做了一个解决方法,以放弃 SSR 为代价暂时解决了这个问题。

通过在我的ContextProvider 上使用dynamic import,我禁用了服务器渲染并且错误消失了。作为奖励,从我默认的深色主题到保存在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 响应

【讨论】:

  • 您是否在整个站点范围内禁用了 ssr?看不到你是如何禁用它的。
  • 我通过将 ssr: false 传递给上面代码中的动态导入选项,仅在我的 DynamicContextProvider 上禁用了 ssr。我还编辑了我的答案,以表明我的解决方案仍然可以在各个页面上使用 SSR。
【解决方案2】:

我只是通过动态导入的默认导出解决了这个错误上下文提供者像这样在_app.tsx.我也在坚持上下文状态本地存储它工作没有问题。

_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>
  );
}

上下文.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;

【讨论】:

    猜你喜欢
    • 2022-10-24
    • 2023-01-30
    • 2022-06-19
    • 2023-01-10
    • 2023-02-04
    • 2022-09-23
    • 2023-01-30
    • 2022-08-20
    • 2022-10-24
    相关资源
    最近更新 更多