React 18: 水合失败,因为初始UI与服务器端渲染的不匹配。

244

我正在尝试在我的应用程序中让SSR正常工作,但是出现了错误:

因为初始UI与在服务器上渲染的内容不匹配,所以水合失败。

演示代码请点击此处

问题的演示请点击此处(打开开发者工具控制台查看错误):

// App.js

 import React from "react";
    
  class App extends React.Component {

  head() {
    return (
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
        <meta name="theme-color" content="#000000" />
        <title>React App</title>
      </head>
    );
  }

  body() {
    return (
      <body>
        <div className="App">
          <h1>Client says Hello World</h1>
        </div>
      </body>
    );
  }

  render() {
    return (
      <React.Fragment>
        {this.head()}
        {this.body()}
      </React.Fragment>
    )
  }
}
export default App;

// index.js

import React from "react";
import * as ReactDOM from "react-dom/client";
import { StrictMode } from "react";

import App from "./App";


// const container = document.getElementById("root");
const container = document.getElementsByTagName("html")[0]

ReactDOM.hydrateRoot(
  container,
  <StrictMode>
    <App />
  </StrictMode>
);

演示中显示的HTML模板由后端提供,并使用以下代码生成:

const ReactDOMServer = require('react-dom/server');

const clientHtml = ReactDOMServer.renderToString(
<StrictMode>
    <App />
</StrictMode>
)

// 将clientHtml发送给客户端

我需要动态生成如App类所示的<head></head>和<body></body>部分。


41
大家是否都同意,这个错误信息比没有任何帮助还要糟糕?! - Kraken
这个错误可能是由于像Dashlane或LastPass这样的扩展程序触发的,因为它们在渲染后注入HTML,从而破坏了服务器和客户端HTML结构之间的比较。请参考这里https://stackoverflow.com/a/75490907/5213598。 - undefined
61个回答

351
我最近在使用NextJS时遇到了同样的问题,但我不确定我的观察是否适用于其他库。
我之前将我的组件包裹在一个不正确的标签中,也就是说,NextJS不喜欢用p标签包裹你的divssections等等,所以它会报错"Hydration failed because the initial UI does not match what was rendered on the server"。
因此,我通过检查我的元素如何相互包裹来解决了这个问题。例如,在使用Material UI时,如果你将Typography组件作为包装器,组件属性的默认值是"p",所以如果你不将组件值更改为语义化的内容,你就会遇到错误。
所以根据我的个人经验和意见,这个问题是由于HTML元素的不正确排列引起的,在NextJS的上下文中解决这个问题,就需要重新评估他们如何排列他们的HTML元素。

import Image from 'next/image'
/**
 * This might give that error
 */
export const IncorrectComponent = ()=>{
  return(
    <p>
      <div>This is not correct and should never be done because the p tag has been abused</div>
      <Image src='/vercel.svg' alt='' width='30' height='30'/>
    </p>
  )
}

/**
 * This will work
 */
export const CorrectComponent = ()=>{
  return(
    <div>
      <div>This is correct and should work because a div is really good for this task.</div>
      <Image src='/vercel.svg' alt='' width='30' height='30'/>
    </div>
  )
}


4
这个关于React的Github评论很好地总结了问题:https://github.com/facebook/react/issues/24519#issuecomment-1122780621。 - rantao
谢谢!在我的例子中,我在表格内使用了tr,当我将我的tr标签与body包装起来时,问题得到了解决。错误路径table>tr | 成功路径table > tbody > tr - Olcay
我的代码问题在于我把一个图像标签放在了链接标签里面。 - Tankiso Thebe
在我的情况下,我将一个 div 用 Next.js 的 <link> 组件包装了起来。 - herbie
哇,在我的情况下,这甚至不是我的代码。我有一个 Chrome 扩展程序,可以在一次点击中下载图像,它会在我所有的图像中引入无效标签,因此 SSR 失败了。 - Diego Milán
显示剩余7条评论

68

导入和运行某些包也可能会导致此错误。例如,当我使用Swiper.js包时就遇到了这个问题。这主要是因为该包在某个地方使用了Window对象。

由于修改包内容本身不是一个好的做法,因此解决此类问题的最佳方法是在DOM加载后仅呈现组件。所以您可以尝试这样做。

const Index = () => {
  const [domLoaded, setDomLoaded] = useState(false);

  useEffect(() => {
    setDomLoaded(true);
  }, []);

  return (
    <>
      {domLoaded && (
        <Swiper>
          <div>Test</div>
        </Swiper>
      )}
    </>
  );
};

export default Index;

2
如果滑块占据了一半的屏幕大小并位于页面顶部,那该怎么办呢?这将创建一个CLS问题,这比水合作用问题更糟糕。 - Sandrin Joy
尝试基于domLoaded设置visibility:hidden而不是使用条件渲染,return ( <div style={domLoaded?undefined:{visibility: 'hidden'}}> <Swiper> <div>Test</div> </Swiper> <div/> ); - singularity
1
天哪!我简直不敢相信这就是答案! - Branislav Lazic
@singularity 为什么能见度应该更好呢? - undefined

33
如果您正在使用表格,则可能错过了。
不正确:
<table>
  <tr>
    <td>a</td>
    <td>b</td>
  </tr>
</table>

正确:

<table>
  <tbody>
    <tr>
      <td>a</td>
      <td>b</td>
    </tr>
  </tbody>
</table>

1
救了我的一天...谢谢! - Mendes

17
这使得应用程序在客户端进行呈现,对我有效:
export default function MyApp({ Component, pageProps }: AppProps) {
  const [showChild, setShowChild] = useState(false);
  useEffect(() => {
    setShowChild(true);
  }, []);

  if (!showChild) {
    return null;
  }

  if (typeof window === 'undefined') {
    return <></>;
  } else {
    return (
      <Provider store={store}>
        <Component {...pageProps} />
      </Provider>
    );
  }
}

我也在使用 NextJS 和 Redux Toolkit


43
我认为这不是一个解决方案,你阻止了服务器渲染组件,并跳过了第一次前端渲染。你的解决方案就是跳过了预载。 - aytek
6
这会使应用程序进行客户端渲染,但并不是一种解决方案。 - Sandrin Joy
有时您需要服务器端渲染,这正是问题所询问的。这不是一个解决方案。 - James Hubert
这不是一个解决方案。 - Lenzman
1
这个解决方案不好,渲染页面源代码(查看源代码)后,在 _next 根 div 中没有加载任何内容。 - Karthi Keyan

17

让我解释一下为什么你的NEXT JS应用程序会出现这个错误。

有些标签不能在另一个标签内使用。 例如,您不能在td标签内使用div。您不能在span标签内使用p标签。其他标签如tableul等也是如此。

您需要进入代码并删除所有使用的无效标签。 在我的情况下,我在span中使用了一个a标签,这是无效的。

这是无效的

<span>       
  <a target="_blank" href="https://askhumans.io"> Login </a>
</span>

This is valid.

      
  <a target="_blank" href="https://askhumans.io"> <span>  Login </span> </a>


你能提供一些关于这个的文档或其他引用吗?如果确实如此,有一个可以指出这些问题的linter或类似工具也会很有帮助。 - maxcountryman
需要通过试错来确定一开始哪个标签没有起作用。 - Smart Coder

10
错误信息误导了我,它并不是关于水合作用,而是标签嵌套错误。我通过删除 HTML 并重新加载页面直到找到问题(二分查找)的方式找出冲突的标签。

在我的情况下,这是由 <Link> 中的 <a> 导致的。我曾经认为 next.js 需要将 <a> 放在链接中,但很明显这已经不是必须的了。

请参见 next.js 有关 <Link> 标签的文档


当您升级到Next.js 13时,可能会发生这种情况。 - Osinachi
我在<Link>标签内嵌套了一个<a>标签,并使用的是Next 13。然而,移除这个嵌套的<a>标签对于我并没有解决问题。我仍然看到错误信息,但只出现在非常少的流量中(约2%)。 - maxcountryman

7

删除<p>标签解决了我在NEXTJS中遇到的类似问题。


可能并不是愚蠢的。在我的情况下,一个组件将其children放在一个Elem中,这个Elem显然是一个<p>元素。当使用dangerously set inner HTML设置children时,出现了水合失败的情况。我摆脱了这个组件,现在它运行得很好了。(我本可以更改组件配置以使用不同的元素,但我意识到我不需要该组件。) - ADTC

5

我使用React 18.2.0Next 12.2.4,并使用以下代码修复了水合作用问题:

import { useEffect, useState } from 'react'
import { Breakpoint, BreakpointProvider } from 'react-socks';
import '../styles/globals.scss'

function MyApp({ Component, pageProps }) {
  const [showChild, setShowChild] = useState(false)

  useEffect(() => {
    setShowChild(true)
  }, [])

  if (!showChild) {
    return null
  }

  return (
    <BreakpointProvider>
      <Component {...pageProps} />
    </BreakpointProvider>
  )
}

export default MyApp

11
由于 useEffect 仅在客户端运行,因此此代码有效地阻止了服务器端渲染的发生。这可能不是您想要的。 - Salem

3

只需打开浏览器,选择Chrome浏览器右上角的三个横杠按钮,再点击“更多工具”,接着选择“清除浏览历史”,最后删除所有的Cookies。

这样就不会再出现错误了。


这确实有效。有时候看起来错误并不是真正的有效错误。对我来说,我清除了我正在开发的本地应用程序上的本地存储,并解决了问题。 - Rob Evans

3

如果您使用HTML标记,您需要正确地放置它们并按正确顺序排列它们。在NEXTJS中。

例如:如果您使用表格。

-> 您必须添加tbody标记。

<!-- Wrong -->

<table>
  <tr>
    <td>Element</td>
    <td>Element</td>
  </tr>
</table>

<!-- Correct Way -->

<table>
  <tbody> <!-- This is a Must -->
    <tr>
      <td>Element</td>
      <td>Element</td>
    </tr>
  </tbody>
</table>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接