渲染
水合错误(Hydration Errors)
为什么会发生
- 不一致(Mismatch):水合期间服务端 HTML 与客户端渲染结果不同
- 常见原因:
Intl(语言/时区)、Date.now()、随机 ID、仅客户端运行的逻辑、特性开关、用户偏好
策略 1 —— 让服务端和客户端一致
- 在服务器上选一个确定性的语言/时区,并在客户端使用相同的
- 数据来源:cookie(首选)或
Accept-Language请求头 - 在服务器上计算一次,并作为初始状态水合
// src/start.ts
import { createStart, createMiddleware } from '@tanstack/react-start'
import {
getRequestHeader,
getCookie,
setCookie,
} from '@tanstack/react-start/server'
const localeTzMiddleware = createMiddleware().server(async ({ next }) => {
const header = getRequestHeader('accept-language')
const headerLocale = header?.split(',')[0] || 'en-US'
const cookieLocale = getCookie('locale')
const cookieTz = getCookie('tz') // set by client later (see Strategy 2)
const locale = cookieLocale || headerLocale
const timeZone = cookieTz || 'UTC' // deterministic until client sends tz
// Persist locale for subsequent requests (optional)
setCookie('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 365 })
return next({ context: { locale, timeZone } })
})
export const startInstance = createStart(() => ({
requestMiddleware: [localeTzMiddleware],
}))// src/routes/index.tsx (example)
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { getCookie } from '@tanstack/react-start/server'
export const getServerNow = createServerFn().handler(async () => {
const locale = getCookie('locale') || 'en-US'
const timeZone = getCookie('tz') || 'UTC'
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone,
}).format(new Date())
})
export const Route = createFileRoute('/')({
loader: () => getServerNow(),
component: () => {
const serverNow = Route.useLoaderData() as string
return <time dateTime={serverNow}>{serverNow}</time>
},
})策略 2 —— 让客户端告诉你它的环境
- 首次访问时,用客户端时区设置 cookie;在那之前 SSR 使用
UTC - 这样做不会引起不一致
import * as React from 'react'
import { ClientOnly } from '@tanstack/react-router'
function SetTimeZoneCookie() {
React.useEffect(() => {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
document.cookie = `tz=${tz}; path=/; max-age=31536000`
}, [])
return null
}
export function AppBoot() {
return (
<ClientOnly fallback={null}>
<SetTimeZoneCookie />
</ClientOnly>
)
}策略 3 —— 改为仅客户端渲染
- 把不稳定的 UI 包在
<ClientOnly>里,避免 SSR 和不一致
import { ClientOnly } from '@tanstack/react-router'
;<ClientOnly fallback={<span>—</span>}>
<RelativeTime ts={someTs} />
</ClientOnly>策略 4 —— 为路由禁用或限制 SSR
- 使用选择性 SSR 来避免在服务器上渲染该组件
export const Route = createFileRoute('/unstable')({
ssr: 'data-only', // or false
component: () => <ExpensiveViz />,
})策略 5 —— 最后的手段:抑制警告
- 对于小的、已知有差异的节点,可以使用 React 的
suppressHydrationWarning
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>检查清单
- 确定性输入:语言、时区、特性开关
- 优先用 cookie 传递客户端上下文;回退到
Accept-Language - 对天生动态的 UI 使用
<ClientOnly> - 当服务端 HTML 无法保持稳定时,使用选择性 SSR
- 避免盲目抑制;
suppressHydrationWarning要谨慎使用