做 #4682(给 client-react 建测试基座)时,用刚建好的基座实测发现,unassigned。
现象
useQuery(object, options) 在 options 里带任何对象/数组/函数字段 时会进入无界重取循环——而这正是它自己 TSDoc 示例的写法。
实测(jsdom + @testing-library/react,fake client 记 data.find 调用次数,渲染后静置 300ms):
| 用法 | 300ms 内 find 调用次数 |
|---|
useQuery('todo_task', { where: { status: 'open' } }) —— 内联 options | 8830 |
const OPTS = {...} 提升到组件外,useQuery('todo_task', OPTS) | 1 |
不是「多取几次」,是以事件循环的速度永远打下去。
成因
packages/client-react/src/data-hooks.tsx:
// L116constfetchData=useCallback(async(isRefetch=false)=>{…},[client,object,query,resolvedFields,resolvedWhere,resolvedSort,resolvedLimit,resolvedOffset,enabled,onSuccess,onError]);// L156useEffect(()=>{fetchData();},[fetchData]);resolvedWhere / resolvedFields / resolvedSort 直接来自 options 的解构,onSuccess / onError 同理。调用方内联传入 → 每次渲染都是新身份 → fetchData 每渲染换身份 → L156 的 effect 每渲染重跑 → setData → 重渲染 → 闭环,无出口。
useQuery('todo_task', {})(全部字段 undefined,都是原始值)不受影响——所以它一直没被发现。触发条件是传 where / fields / orderBy / onSuccess / onError 中的任意一个,也就是绝大多数真实用法。
同一形状的 useCallback 依赖链在 data-hooks.tsx:592 和 metadata-hooks.tsx:136 也存在,需要一并核查。
影响
处置方向
不要用「让调用方自己 useMemo」当修复——文档示例就是内联写法,把正确性押在调用方的记忆力上等于没修。
按值而非按身份做依赖。可选:对 where/fields/orderBy 做稳定序列化(如 JSON.stringify 或结构化哈希)作为依赖;onSuccess/onError 存进 ref(它们是副作用回调,本就不该参与重取决策)。
修复必须带回归测试,基座已经就位(#4682 / PR #4692),断言直接用上表那两个数字的形状(内联 options 稳定后 find 只该被调用 1 次)。
复现
基座合入后,packages/client-react/ 下:
constfind=vi.fn(async()=>({data: [],total: 0}));constclient={data: { find,query: find},events: {},setLocale: vi.fn()}asunknownasObjectStackClient;functionC(){useQuery('todo_task',{where: {status: 'open'}});returnnull;}render(<ObjectStackProviderclient={client}><C/></ObjectStackProvider>);awaitnewPromise(r=>setTimeout(r,300));expect(find.mock.calls.length).toBe(1);// 实测 8830
做 #4682(给 client-react 建测试基座)时,用刚建好的基座实测发现,unassigned。
现象
useQuery(object, options)在 options 里带任何对象/数组/函数字段 时会进入无界重取循环——而这正是它自己 TSDoc 示例的写法。实测(jsdom + @testing-library/react,fake client 记
data.find调用次数,渲染后静置 300ms):find调用次数useQuery('todo_task', { where: { status: 'open' } })—— 内联 optionsconst OPTS = {...}提升到组件外,useQuery('todo_task', OPTS)不是「多取几次」,是以事件循环的速度永远打下去。
成因
packages/client-react/src/data-hooks.tsx:resolvedWhere/resolvedFields/resolvedSort直接来自options的解构,onSuccess/onError同理。调用方内联传入 → 每次渲染都是新身份 →fetchData每渲染换身份 → L156 的 effect 每渲染重跑 →setData→ 重渲染 → 闭环,无出口。useQuery('todo_task', {})(全部字段undefined,都是原始值)不受影响——所以它一直没被发现。触发条件是传where/fields/orderBy/onSuccess/onError中的任意一个,也就是绝大多数真实用法。同一形状的
useCallback依赖链在data-hooks.tsx:592和metadata-hooks.tsx:136也存在,需要一并核查。影响
tsc完全看不见 —— 这正是 client-react 没有任何测试基座:8 个公开 hook 的行为全靠 typecheck 兜底 #4682 存在的理由。处置方向
不要用「让调用方自己
useMemo」当修复——文档示例就是内联写法,把正确性押在调用方的记忆力上等于没修。按值而非按身份做依赖。可选:对
where/fields/orderBy做稳定序列化(如JSON.stringify或结构化哈希)作为依赖;onSuccess/onError存进 ref(它们是副作用回调,本就不该参与重取决策)。修复必须带回归测试,基座已经就位(#4682 / PR #4692),断言直接用上表那两个数字的形状(内联 options 稳定后
find只该被调用 1 次)。复现
基座合入后,
packages/client-react/下: