如何解决React/Remix应用中与日期相关的Hydration错误?
Remix 水合错误:时区差异导致服务端与客户端渲染内容不匹配
我在业余项目中学习React服务端渲染(使用Remix 1.10版本),遇到了棘手的水合错误:代码能运行但存在缺陷,页面加载时元素闪烁,控制台抛出多个错误:
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.Warning: Text content did not match. Server: "1/29/2023, 10:44:09 AM" Client: "1/29/2023, 12:44:09 PM"
问题根源是服务器处于UTC时区,客户端时区不确定(本次测试为GMT+2),直接在客户端用toLocaleString格式化时间会导致服务端渲染的内容和客户端不一致。我不想修改服务器时区,这不是合理方案。
最简复现代码:
// routes/example.tsx import { useLoaderData } from "@remix-run/react" import {json, LoaderArgs } from "@remix-run/server-runtime" export async function loader({ request }: LoaderArgs) { const timestampFromDB = "2023-01-29T10:44:09.672Z" return json({ time: timestampFromDB }) } export default function HydrationError() { const loaderData = useLoaderData<typeof loader>() const time = new Date(loaderData.time) const stamp = time.toLocaleString("en-US") return ( <div> Time: <time>{stamp}</time> </div> ) }
解决思路
要避免水合错误,必须保证服务端和客户端渲染的内容完全一致,以下是两种可行方案:
方案1:服务端格式化时间并指定UTC时区,客户端直接复用结果
直接在loader中完成时间格式化,强制指定UTC时区,让服务端和客户端渲染的字符串完全一致,从根源消除差异。
修改后的代码:
// routes/example.tsx import { useLoaderData } from "@remix-run/react" import {json, LoaderArgs } from "@remix-run/server-runtime" export async function loader({ request }: LoaderArgs) { const timestampFromDB = "2023-01-29T10:44:09.672Z" // 服务端格式化时指定UTC时区 const formattedTime = new Date(timestampFromDB).toLocaleString("en-US", { timeZone: "UTC" }) return json({ time: formattedTime }) } export default function HydrationFixed() { const loaderData = useLoaderData<typeof loader>() return ( <div> Time: <time>{loaderData.time}</time> </div> ) }
方案2:水合完成后再切换为客户端本地时区时间
如果需要显示用户本地时区的时间,可以先渲染服务端返回的UTC格式化字符串,等水合完成后再更新为本地时区时间,既保证水合成功,又满足本地时间显示需求。
修改后的代码:
// routes/example.tsx import { useLoaderData, useEffect, useState } from "@remix-run/react" import {json, LoaderArgs } from "@remix-run/server-runtime" export async function loader({ request }: LoaderArgs) { const timestampFromDB = "2023-01-29T10:44:09.672Z" // 服务端返回UTC格式化的时间 const utcTime = new Date(timestampFromDB).toLocaleString("en-US", { timeZone: "UTC" }) return json({ utcTime, rawTime: timestampFromDB }) } export default function HydrationFixed() { const loaderData = useLoaderData<typeof loader>() const [displayTime, setDisplayTime] = useState(loaderData.utcTime) useEffect(() => { // 水合完成后,切换为客户端本地时区的时间 const localTime = new Date(loaderData.rawTime).toLocaleString("en-US") setDisplayTime(localTime) }, [loaderData.rawTime]) return ( <div> Time: <time>{displayTime}</time> </div> ) }
方案说明
- 方案1适合不需要显示客户端本地时区的场景,实现简单,完全避免水合问题。
- 方案2适合需要展示用户本地时区时间的场景,先保证水合成功,再在客户端静默更新内容,几乎不会出现闪烁问题。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

