修复sortDate导致的服务端渲染与客户端HTML内容不匹配报错
Text content does not match server-rendered HTML 报错修复 问题背景
触发报错的业务代码如下:
<span> {sortDate(e.createdAt)} </span>
- 代码中调用的
sortDate函数用于返回「10秒前」格式的相对时间文本 - 报错根因:服务端渲染时
sortDate计算得到的相对时间(例如18秒前),和客户端完成资源加载、执行水合时因时间流逝重新计算的结果(例如19秒前)不一致,触发框架的水合DOM校验失败 - 约束:
sortDate已在10+页面复用,不接受基于useEffect逐组件改造的方案
修复方案
所有方案均只需要修改sortDate工具函数本身,不需要改动业务侧已有调用代码。
方案1:水合阶段对齐时间粒度(改造成本最低)
修改sortDate内部逻辑,水合完成前不输出精确到秒的时间,统一使用不会因几秒传输延迟产生变化的时间粒度,水合完成后再自动切换为精确时间展示:
// 全局水合标记,仅页面首次加载时为false let appHydrated = false; export function sortDate(timestamp) { const now = Date.now(); const diff = now - timestamp; // 水合阶段使用粗粒度时间,保证两端计算结果一致 if (!appHydrated) { if (diff < 60 * 1000) return '刚刚'; if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}分钟前`; // 小时、天及以上粒度的时间差,不会因几秒延迟产生变化,无需特殊处理 } // 客户端水合完成后,标记状态并触发一次全局更新展示精确时间 if (!appHydrated && typeof window !== 'undefined') { appHydrated = true; queueMicrotask(() => { window.dispatchEvent(new Event('app-time-sync')); }); } // 原有精确计算逻辑保持不变 if (diff < 60 * 1000) return `${Math.floor(diff / 1000)}秒前`; if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}分钟前`; if (diff < 86400 * 1000) return `${Math.floor(diff / 3600000)}小时前`; // 后续天、月、年的计算逻辑保留原有实现即可 } // 全局绑定时间更新事件,业务侧如果有定时刷新时间的逻辑可以监听这个事件 if (typeof window !== 'undefined') { window.addEventListener('app-time-sync', () => { // 这里可以对接项目已有的全局状态,触发所有用到时间的组件重渲染 }, { once: true }); }
方案2:统一两端首次渲染的时间基准(保留秒级精度)
如果业务要求水合阶段也要展示精确到秒的时间,可以在服务端渲染时将渲染时刻的时间戳注入全局,客户端首次水合计算时间差时统一使用该时间戳,水合完成后再切换为本地真实时间:
- 服务端渲染入口注入全局时间戳(以Next.js为例,在根layout或自定义Document中添加):
<script dangerouslySetInnerHTML={{ __html: `window.__SSR_RENDER_AT__ = ${Date.now()}` }} />
- 修改
sortDate的时间获取逻辑:
let appHydrated = false; function getCurrentRenderTime() { // 服务端直接返回当前时间 if (typeof window === 'undefined') return Date.now(); // 水合完成前使用服务端注入的时间,保证两端计算基准一致 if (!appHydrated) return window.__SSR_RENDER_AT__; // 水合完成后返回真实本地时间 return Date.now(); } export function sortDate(timestamp) { const diff = getCurrentRenderTime() - timestamp; // 原有精确计算逻辑完全不需要改动 if (diff < 60 * 1000) return `${Math.floor(diff / 1000)}秒前`; // 其余时间粒度计算逻辑保持原样 } // 水合完成后标记状态并触发更新 if (typeof window !== 'undefined') { window.addEventListener('load', () => { appHydrated = true; window.dispatchEvent(new Event('app-time-sync')); }, { once: true }); }
避坑提示
- 不要直接给元素加
suppressHydrationWarning属性绕过报错,该属性仅能临时掩盖单行文本的水合警告,会导致其他真实DOM不匹配问题无法被及时发现,长期维护隐患极大 - 所有相对时间计算逻辑必须收口到
sortDate工具函数,禁止在业务组件中单独编写时间差计算逻辑,避免出现漏改场景
内容的提问来源于stack exchange,提问作者eldernyPro
相关产品推荐
相关产品推荐

