
React API 误调用问题的排查及修复
- Published On
最前/背景-1
近期处理一个 SSR 站点的批量用户头像不能正确显示问题,在项目的预发布分支中,这个问题是近期出现。吾辈定位到“头像显示”组件中,因为用户头像使用 RadixUI/Avatar 组件,一开始的排查方向是该库的兼容性出现了问题(最近该项目有作部分依赖升级),并且组件代码中有通过 Avatar#onLoadingStatusChange 的事件回调,来用作判断条件:用户头像加载完成前的 loading 效果图显示,在此函数中打上 log 日志,输出头像的加载状态字符串,
问题初步浮现:控制台中,一直重复出现同一个头像的 loading 状态,偶尔夹杂 loaded 状态,进而页面的 CPU 占用率明显升高,页面卡顿明显,完全不能上线使用的程度。
通过艰难排查,最终发现问题……
最小复现该问题的demo
吾辈在 Stackblitz 平台 搭建了最小复现该问题的demo,读者有兴趣的话,在阅读下面的正文前,可以先尝试能否自己排查并修复问题(排查范围聚焦在 /src/draft/
下即可)
demo-id
背景-2
服务端中,对于某些组件,我们出于某些原因不希望交由服务端渲染,我们可以定义一个 clientOnly 组件,可以参考这份开源库的源码实现:
// import ...
type Props = {
children(): React.ReactNode;
fallback?: React.ReactNode;
};
export function ClientOnly({ children, fallback = null }: Props) {
return useHydrated() ? <>{children()}</> : <>{fallback}</>;
}
正文
回到本 SSR 站点项目, 批量用户头像的随机顺序显示的组件,顶层也使用了 ClientOnly 包裹,只在客户端渲染,一番排查不能在组件内部找出原因,吾辈最终看向了该组件顶层标签(即 ClientOnly)的实现:
目前的版本,为了兼容 children 直接以 ReactNode 的形式传递,以上面的范例源码为例,作了如下修改:
// import ...
type Props = {
children: React.FC | React.ReactNode;
fallback?: React.ReactNode;
};
export function ClientOnly({ children, fallback = null }: Props) {
const tag = useHydrated();
if (!tag) return fallback;
if (typeof children === "function") {
return React.createElement(children);
}
return children;
}
吾辈的直觉,注意到了 React.createElement
的使用,虽然 React 官方说该 api 是 JSX 的完全等同写法,不过,吾辈觉得这里的调用,可能出现了丢失上下文,或者丢失组件函数实例化后的唯一签名 的问题。
所以当 ClientOnly 组件重渲染后,作为子组件的 Avatar 就表现为状态不断的“重置”回起始的 “loading” 状态。
(上面的 Demo 示例完全复现了这样的情况,可以直观观察到问题表现)
而后尝试直接显式写作: return someCondition ? children() : children
,才终于解决了该问题。
启发
所以,一方面的,在源码中,谨慎的使用一些框架基础 API,因为不能保证用户以如何的形式调用。比如上文中,ClientOnly 的一个修改版本中,因为错误使用 React#createElement,导致了页面的严重重渲染问题。
而后,就是对于组件的条件式渲染,没有特别传参情况下(或者进一步需求的情况下),可以使用更直观方式:
<ClientOnly>
<div>静态内容</div>
</ClientOnly>
// 另外种调用形式:
<ClientOnly>
{() => <div>客户端渲染内容</div>}
</ClientOnly>
更正:仍然有支持第二种写法(传递函数)的必要:
有几个原因(综合问询 ClaudeAI 结果,可自行验证):
- 延迟执行:
// ❌ 直接传递节点 - 会立即执行所有代码
<ClientOnly>
<div>{expensiveCalculation()}</div>
<ThirdPartyComponent />
</ClientOnly>
// ✅ 函数式写法 - 只在客户端水合后才执行
<ClientOnly>
{() => (
<div>{expensiveCalculation()}</div>
<ThirdPartyComponent />
)}
</ClientOnly>
- 避免服务端不必要的计算和导入:
// ❌ 即使在服务端也会执行导入
<ClientOnly>
<SomeClientOnlyComponent /> {/* 服务端会尝试导入这个组件 */}
</ClientOnly>
// ✅ 服务端不会执行函数内部的代码
<ClientOnly>
{() => {
// 这些代码只会在客户端执行
const data = window.localStorage.getItem('key');
return <SomeClientOnlyComponent data={data} />;
}}
</ClientOnly>
React API 误调用问题的排查及修复
https://infen.cc/loc-blog/25_wrong-used-react-api[Copy]转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。