这是用户在 2024-12-21 15:06 为 https://www.jacobparis.com/content/remix-hydration-errors 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Jacob Paris  雅各布·帕里斯
← Back to all content
← 返回全部内容

Solve React hydration errors in Remix/Next apps
解决 Remix/Next 应用程序中的 React 水合错误

Hydration errors affect every server-rendered React app. This is not a bug in Next.js or Remix, but a fundamental issue in the way React's server-side rendering works.
水合错误会影响每个服务器渲染的 React 应用程序。这不是 Next.js 或 Remix 中的错误,而是 React 的服务器端渲染工作方式的一个基本问题。

People usually describe hydration errors as mismatches between the HTML that the server generates and what the browser generates, but that's not the whole story.
人们通常将水合错误描述为服务器生成的 HTML 与浏览器生成的 HTML 之间的不匹配,但这并不是全部。

To explain this, lets look at how React's server-side rendering works. Most implementations follow the same steps.
为了解释这一点,让我们看看 React 的服务器端渲染是如何工作的。大多数实现都遵循相同的步骤。

  1. The server renders the page and serves HTML to the client.
    服务器呈现页面并向客户端提供 HTML。
  2. The browser loads the page and React starts running.
    浏览器加载页面并且 React 开始运行。
  3. React re-renders the page and generates its own HTML.
    React 重新渲染页面并生成自己的 HTML。
  4. React then compares the HTML it generated with the HTML the browser is displaying.
    然后 React 会将它生成的 HTML 与浏览器显示的 HTML 进行比较。
  5. If the HTML matches, great! Otherwise, React will throw a hydration error.
    如果 HTML 匹配,那就太好了!否则,React 将抛出水合错误。

Since React is comparing the HTML it generated with the HTML the browser is displaying, anything that changed the HTML between the time the server sent it and the time React started running can cause a hydration error.
由于 React 会将其生成的 HTML 与浏览器显示的 HTML 进行比较,因此在服务器发送 HTML 的时间和 React 开始运行的时间之间任何更改 HTML 的行为都可能导致水合错误。

What do hydration errors look like?
水合错误是什么样子的?

The main symptom of a hydration error is that React bails on its server-rendered content and does a full client-side render. This can cause the page to flicker and a full refetch of data.
水合错误的主要症状是 React 放弃其服务器渲染的内容并执行完整的客户端渲染。这可能会导致页面闪烁并完全重新获取数据。

React suppresses errors in production, so these may come up as "Minified React Error #418" or "Minified React Error #425". There are likely more of these but I won't attempt to make a comprehensive list.
React 会抑制生产中的错误,因此这些错误可能会显示为“Minified React Error #418”或“Minified React Error #425”。可能还有更多,但我不会尝试列出一个全面的列表。

In development, you will see a marginally more helpful error message.
在开发过程中,您会看到一条稍微有用的错误消息。

  • Text content does not match server-rendered HTML
    文本内容与服务器呈现的 HTML 不匹配
  • Hydration failed because the initial UI does not match what was rendered on the server.
    Hydration 失败,因为初始 UI 与服务器上呈现的内容不匹配。
  • Warning: Expected server HTML to contain a matching <head> in <html>.
    警告:预期服务器 HTML 在 <html> 中包含匹配的 <head>

In this article, we'll look at some of the most common causes of hydration errors and how to debug them.
在本文中,我们将了解水合错误的一些最常见原因以及如何调试它们。

🔥 Tip: Diff the HTML to find the exact cause
🔥 提示:比较 HTML 以找到确切的原因

Before we move on to common reasons for hydration errors, one super handy trick is to diff the HTML that the server sends with the HTML that React generates.
在我们继续讨论水合错误的常见原因之前,一个超级方便的技巧是将服务器发送的 HTML 与 React 生成的 HTML 进行比较。

There are three places to look for this.
有三个地方可以找到这个。

  1. The network tab in your browser's dev tools will show you the HTML that the server sends.
    浏览器开发工具中的网络选项卡将向您显示服务器发送的 HTML。
  2. If you remove the <Scripts /> in your root.tsx, Remix will not hydrate and you can use View Source to see the HTML that was present right before hydration.
    如果您删除 root.tsx 中的 <Scripts /> ,Remix 将不会水化,您可以使用“查看源代码”来查看水化之前存在的 HTML。
  3. With the scripts running normally, use View Source to see the HTML that React generated post-hydration.
    在脚本正常运行的情况下,使用“查看源代码”查看 React 水合后生成的 HTML。

If you diff these three HTML sources, you can usually find the exact cause of the hydration error.
如果您比较这三个 HTML 源,通常可以找到水合错误的确切原因。

There are many online HTML diff tools, just google one and paste in the HTML from two of the three sources.
有许多在线 HTML 比较工具,只需 google 一个并粘贴来自三个来源中的两个来源的 HTML。

Browser extensions or adblockers
浏览器扩展程序或广告拦截器

Browser extensions often have permission to modify the live page content of any website, including your app. This can cause hydration errors if the extension modifies the HTML before React has a chance to compare it.
浏览器扩展通常有权修改任何网站(包括您的应用程序)的实时页面内容。如果扩展在 React 有机会比较 HTML 之前修改了 HTML,则可能会导致水合错误。

Try your app in an incognito/private browsing window with all extensions disabled. If the error goes away, you know that an extension is causing the problem.
在禁用所有扩展的情况下在隐身/私密浏览窗口中尝试您的应用程序。如果错误消失,您就知道是扩展引起了问题。

There are also desktop level ad-blockers and security software that may cause similar issues. If you're running one of these, try disabling it or testing on another device. You may be able to add an exception in the tool for your website.
还有桌面级广告拦截器和安全软件可能会导致类似问题。如果您正在运行其中一个,请尝试禁用它或在另一台设备上进行测试。您也许可以在该工具中为您的网站添加例外。

If the extension is only modifying the head of your document and not the body, hydration errors caused by this are a bug in React 18 and have been solved in React 18.3 Canary. Next.js users will have this automatically, but Remix users could try updating to that version or newer to see if it fixes the issue.
如果扩展仅修改文档的头部而不是正文,则由此引起的水合错误是 React 18 中的一个错误,并且已在 React 18.3 Canary 中解决。 Next.js 用户将自动获得此版本,但 Remix 用户可以尝试更新到该版本或更高版本,看看是否可以解决问题。

Alternatively, Remix users can use remix-island to mount Remix to a specific div in the body, which will prevent changes to the head from causing hydration errors.
或者,Remix 用户可以使用 remix-island 将 Remix 安装到身体中的特定 div,这将防止头部变化导致水合错误。

Invalid HTML  HTML 无效

The browser does a lot of error correction to make sure that pages render reasonably even when the HTML is malformed.
浏览器会进行大量错误纠正,以确保即使 HTML 格式错误也能合理呈现页面。

For example, if you illegally nest a <div> inside a <p>, the browser will move the <div> outside of the <p> to make the HTML valid.
例如,如果您非法地将 <div> 嵌套在 <p> 中,浏览器会将 <div> 移到 <p> 之外以使 HTML 有效。

Nested forms are prohibited in HTML, so if you have a form inside a form, the browser will move the nested form outside of the parent form to become its sibling instead.
HTML 中禁止嵌套表单,因此如果表单内有一个表单,浏览器会将嵌套表单移到父表单之外,成为其同级表单。

When React hydrates, it will compare the HTML it generated with the HTML the browser is displaying. If the browser moved elements around, React will throw a hydration error.
当 React 水合时,它会将生成的 HTML 与浏览器显示的 HTML 进行比较。如果浏览器移动元素,React 将抛出水合错误。

The solution here is to write React code that outputs valid HTML.
这里的解决方案是编写输出有效 HTML 的 React 代码。

The Remix Dev Tools will now detect at development time whether you're outputting invalid HTML and warn you about it, with a clickable link to the offending line of code in your editor.
Remix 开发工具现在将在开发时检测您是否输出无效 HTML 并发出警告,并在编辑器中提供指向违规代码行的可点击链接。

Third party scripts or non-react packages
第三方脚本或非 React 包

Third party scripts may also modify the HTML on the page. This is especially common with analytics scripts like HotJar or Google Tag Manager (GTM). If you're using a third party script, try removing it and see if the error goes away.
第三方脚本也可能修改页面上的 HTML。这在 HotJar 或 Google 标签管理器 (GTM) 等分析脚本中尤其常见。如果您使用的是第三方脚本,请尝试将其删除并查看错误是否消失。

If this is the issue, you may be able to run the script after React has finished hydrating. For example, you can use useEffect to run the script after the first render, or use the Remix Utils ExternalScripts utility to do this for you.
如果这是问题所在,您也许可以在 React 完成水合作用后运行脚本。例如,您可以使用 useEffect 在第一次渲染后运行脚本,或者使用 Remix UtilsExternalScripts 实用程序来为您执行此操作。

The next/script package is the canonical solution for Next.js.
next/script 包是 Next.js 的规范解决方案。

CSS in JS libraries
JS 库中的 CSS

CSS in JS is a methodology for styling React apps that defines styles as a side effect of rendering.
JS 中的 CSS 是一种用于设计 React 应用程序样式的方法,它将样式定义为渲染的副作用。

In order to get a stylesheet that contains all the styles for a page, React needs to fully render the page to see which components are used and what styles they need. If it tried to stream the HTML, the browser would start displaying the page before React had a chance to generate the stylesheet.
为了获得包含页面所有样式的样式表,React 需要完全渲染页面以查看使用了哪些组件以及它们需要什么样式。如果它尝试流式传输 HTML,浏览器将在 React 有机会生成样式表之前开始显示页面。

Chakra, Emotion, and Material UI are popular CSS in JS libraries that have this issue.
Chakra、Emotion 和 Material UI 是存在此问题的 JS 库中流行的 CSS。

A common solution is to double-render the page on the server
常见的解决方案是在服务器上双重渲染页面
. After the first render, it can extract all the styles it needs, and then the second render can be streamed to the browser.
。第一次渲染后,它可以提取所需的所有样式,然后第二次渲染可以流式传输到浏览器。

If you aren't committed to CSS in JS, alternative styling solutions like Tailwind or Vanilla Extract work much better with server rendering and will not cause hydration issues.
如果您不致力于在 JS 中使用 CSS,Tailwind 或 Vanilla Extract 等替代样式解决方案可以更好地与服务器渲染配合使用,并且不会导致水合作用问题。

Character encoding  字符编码

Character encoding issues usually present obvious error messages, so they're easy to diagnose.
字符编码问题通常会出现明显的错误消息,因此很容易诊断。

txt
[Error] Warning: Text content did not match.
Server: "â€"
Client: "’"

This happens when you have a mismatch between the character encoding of the server and the client. The most common cause is that the server is sending UTF-8 encoded text, but the client is interpreting it as ISO-8859-1.
当服务器和客户端的字符编码不匹配时,就会发生这种情况。最常见的原因是服务器正在发送 UTF-8 编码文本,但客户端将其解释为 ISO-8859-1。

To fix this, add the following meta tag to the <head> of your HTML.
要解决此问题,请将以下元标记添加到 HTML 的 <head> 中。

xml
<meta
http-equiv="Content-Type"
content="text/html;charset=utf-8"
/>

Timezone mismatches  时区不匹配

Likely the most notorious culprit is the Date object. Dates are complicated, and the less you have to deal with them the happier your life will be.
最臭名昭著的罪魁祸首可能是 Date 对象。约会很复杂,你处理的事情越少,你的生活就会越幸福。

When javascript tries to create a date, it will use the timezone of the machine it's running on. This means that if you create a date on the server and send it to the client, the client will create a date in its own timezone, which will be different from the server's timezone.
当 javascript 尝试创建日期时,它将使用其运行所在计算机的时区。这意味着,如果您在服务器上创建日期并将其发送到客户端,则客户端将在自己的时区中创建日期,该时区将与服务器的时区不同。

If you're trying to render a date, you'll get these hydration issues close to midnight every day when the server date becomes different than the client date.
如果您尝试渲染日期,那么当服务器日期与客户端日期不同时,每天接近午夜时您都会遇到这些水合作用问题。

  • The easiest solution is to format the date as a string on the server and send it as regular text.
    最简单的解决方案是将日期格式化为服务器上的字符串并将其作为常规文本发送。
  • The client can send timezone/locale information to the server and the server can use that to create a fake date that will match the client's timezone during hydration.
    客户端可以将时区/区域设置信息发送到服务器,服务器可以使用该信息创建一个伪造的日期,该日期将在水合作用期间与客户端的时区相匹配。
  • More solutions in the rendering based on client data section
    更多解决方案在基于客户端数据的渲染部分

Non-idempotent functions like UUIDs
UUID 等非幂等函数

If you're using a non-idempotent function like uuid to generate a unique ID, you may get hydration errors. This is because the server and client will generate different IDs.
如果您使用 uuid 等非幂等函数来生成唯一 ID,则可能会出现水合错误。这是因为服务器和客户端会生成不同的ID。

You can either generate the ID on the server and send to the client, or pass a seed to the function so that the server and client generate the same ID.
您可以在服务器上生成 ID 并将其发送到客户端,或者将种子传递给函数,以便服务器和客户端生成相同的 ID。

If the random value you're trying to generate is an ID or key for a specific component, React 18 includes a useId hook.
如果您尝试生成的随机值是特定组件的 ID 或密钥,React 18 包含 useId 挂钩。

Rendering based on client data
根据客户数据进行渲染

Some data only exists on the client and will not be available when the server renders the page for the first time.
有些数据只存在于客户端,当服务器第一次渲染页面时将不可用。

For example, you may have an input that takes a default value from local storage. If you render the input empty on the server and then populate it on the client, you'll get a hydration error and an unsightly flash of empty input.
例如,您可能有一个从本地存储获取默认值的输入。如果您在服务器上将输入渲染为空,然后将其填充到客户端上,您将收到水合错误和难看的空输入闪烁。

There is no way for the server to know what the client wants to render, so your solution is to find a way to hide the element on the server and during the initial hydration render.
服务器无法知道客户端想要渲染什么,因此您的解决方案是找到一种方法来隐藏服务器上和初始水合渲染期间的元素。

The useHydrated hook from Remix Utils will give you an isHydrated boolean that you can use to conditionally render the element.
Remix Utils 中的 useHydrated 挂钩将为您提供一个 isHydrated 布尔值,您可以使用它来有条件地渲染元素。

This is the easiest solution but has its own caveats
这是最简单的解决方案,但有其自身的注意事项

  • It still creates a flash of empty content, though you can use a fade effect to make it less jarring.
    尽管您可以使用淡入淡出效果来使其不那么刺耳,但它仍然会产生一闪而过的空内容。
  • The elements will not appear at all if javascript fails to load, breaking the progressive enhancement story.
    如果 JavaScript 无法加载,这些元素将根本不会出现,从而打破了渐进增强的故事。

My preferred solution is to use a ProgressiveClientOnly component that will hide the server rendered content with CSS and swap it for the client content if Javascript is available, else it will show the server content.
我的首选解决方案是使用 ProgressiveClientOnly 组件,该组件将使用 CSS 隐藏服务器渲染的内容,并在 Javascript 可用时将其交换为客户端内容,否则它将显示服务器内容。

Professional headshot
Moulton
Moulton  莫尔顿

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter
嘿!我是一名开发者、设计师和数字游牧者,用 Remix 打造酷炫的东西,同时我还在撰写 Remix 社区通讯 Moulton

About once per month, I send an email with:
大约每月一次,我会发送一封电子邮件,其中包含:

  • New guides and tutorials   新的指南和教程
  • Upcoming talks, meetups, and events
    即将举行的会谈、聚会和活动
  • Cool new libraries and packages
    很酷的新库和包
  • What's new in the latest versions of Remix
    最新版本 Remix 中的新增功能

Stay up to date with everything in the Remix community by entering your email below.
在下面输入您的电子邮件,了解 Remix 社区的最新动态。

Unsubscribe at any time.  随时取消订阅。