The problem: useDelayState schedules a delayed setValue via setTimeout / raf and defines a cancelPending callback, but cancelPending is only ever called from setDelayValue. There is no cleanup effect, so a component that unmounts with a pending update leaves the timer running and setValue fires against an unmounted tree.
In a browser that is not a problem, but under a jsdom test runner it throws an error. Vitest tears the jsdom environment down when a test file finishes; setTimeout on globalThis is Node's (vitest does not shadow it), so the timer survives, fires with window already deleted, and React DOM throws while resolving update priority — failing the whole run and blaming whichever test file happened to be running:
ReferenceError: window is not defined
resolveUpdatePriority react-dom/cjs/react-dom-client.development.js:1308:7
requestUpdateLane react-dom-client.development.js:16345:11
dispatchSetState react-dom-client.development.js:9126:14
Timeout._onTimeout @rc-component/util/es/hooks/useDelayState.js:30:51
listOnTimeout node:internal/timers:635:17
Unmounting alone is not enough, React still routes dispatchSetState through requestUpdateLane, so the timer genuinely has to be cancelled.
This is easy to hit through antd 6.6: form/hooks/useDebounce calls setCacheValue(value, { ms: value.length ? 0 : 10 }) on mount, so every Form.Item leaves a pending timer; Button with an object loading and Slider / Typography / Upload do the same.
Deps: @rc-component/util@1.12.0, antd@6.6.0, react@19.2.8, react-dom@19.2.8, vitest@4.1.10, jsdom@30.0.1.
The problem:
useDelayStateschedules a delayedsetValueviasetTimeout/rafand defines acancelPendingcallback, butcancelPendingis only ever called fromsetDelayValue. There is no cleanup effect, so a component that unmounts with a pending update leaves the timer running and setValue fires against an unmounted tree.In a browser that is not a problem, but under a jsdom test runner it throws an error. Vitest tears the jsdom environment down when a test file finishes;
setTimeoutonglobalThisis Node's (vitest does not shadow it), so the timer survives, fires with window already deleted, and React DOM throws while resolving update priority — failing the whole run and blaming whichever test file happened to be running:Unmounting alone is not enough, React still routes
dispatchSetStatethroughrequestUpdateLane, so the timer genuinely has to be cancelled.This is easy to hit through antd 6.6:
form/hooks/useDebouncecallssetCacheValue(value, { ms: value.length ? 0 : 10 })on mount, so everyForm.Itemleaves a pending timer;Buttonwith an objectloadingandSlider/Typography/Uploaddo the same.Deps:
@rc-component/util@1.12.0,antd@6.6.0,react@19.2.8,react-dom@19.2.8,vitest@4.1.10,jsdom@30.0.1.