feat: add useDelayState hook#796
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
React Doctor found no issues. 🎉
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new useDelayState hook, which allows state updates to be delayed by a frame or a specified millisecond duration, along with comprehensive tests. Feedback was provided to address TypeScript compilation issues with read-only refs by correctly typing useRef as mutable, and to improve safety by replacing non-null assertions with explicit null checks before clearing timers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const rafRef = React.useRef<number>(null); | ||
| const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(null); | ||
|
|
||
| const cancelPending = useEvent(() => { | ||
| raf.cancel(rafRef.current!); | ||
| clearTimeout(timeoutRef.current!); | ||
| rafRef.current = null; | ||
| timeoutRef.current = null; | ||
| }); |
There was a problem hiding this comment.
问题分析
- TypeScript 类型只读问题:在 TypeScript 中,使用
React.useRef<T>(null)会被推导为RefObject<T>,其current属性是只读的(readonly current: T | null)。因此,直接对rafRef.current和timeoutRef.current进行赋值(如第 28、29 行)在严格模式或标准 React 类型定义下会触发编译错误。为了使其可变,应当将类型声明为React.useRef<T | null>(null),这样会被推导为MutableRefObject。 - 非空断言(
!)的安全隐患:在cancelPending中使用了rafRef.current!和timeoutRef.current!。虽然在运行时clearTimeout(null)不会报错,但使用非空断言是不够安全的防御性编程实践。建议在取消前先进行非空判断,从而安全地移除!并提升代码健壮性。
const rafRef = React.useRef<number | null>(null);
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const cancelPending = useEvent(() => {
if (rafRef.current !== null) {
raf.cancel(rafRef.current);
rafRef.current = null;
}
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
});
✅ Preview is ready!
↩️ Previous: ⚡️ 🤖 Powered by surge-preview |
|||||||||||||||
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #796 +/- ##
==========================================
+ Coverage 86.42% 86.72% +0.29%
==========================================
Files 39 40 +1
Lines 1068 1092 +24
Branches 388 395 +7
==========================================
+ Hits 923 947 +24
Misses 143 143
Partials 2 2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough新增 Changes延迟状态更新
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ReactComponent
participant useDelayState
participant raf_or_setTimeout
ReactComponent->>useDelayState: setDelayValue(nextValue, immediatelyOrDelay)
useDelayState->>useDelayState: cancelPending()
useDelayState->>raf_or_setTimeout: schedule frame or timeout
raf_or_setTimeout->>useDelayState: execute pending update
useDelayState->>ReactComponent: render updated value
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| ): [T, SetDelayState<T>] { | ||
| const [value, setValue] = React.useState(defaultValue); | ||
| const rafRef = React.useRef<number | null>(null); | ||
| const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null); |
There was a problem hiding this comment.
两个 ref 有点多余,你可以直接 useRef<[isRaf: boolean, delay: number]> // delay when is raf means delay raf times, or ms will be delay ms
| }, | ||
| ); | ||
|
|
||
| React.useEffect(() => cancelPending, [cancelPending]); |
There was a problem hiding this comment.
no need cancel since setState will do nothing when unmount
There was a problem hiding this comment.
Just remove this effect hooks
| } else if ('ms' in delayConfig) { | ||
| delayRef.current = [ | ||
| false, | ||
| window.setTimeout(() => setValue(nextValue), delayConfig.ms), |
There was a problem hiding this comment.
不要使用 window,你如果定义搞不定。直接使用 typeof setTimeout
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/hooks/useDelayState.ts`:
- Around line 46-55: Resolve the mismatch between the PR’s “cleanup on unmount”
requirement and the current useDelayState implementation: either restore unmount
cleanup that cancels pending delayRef timers/raf chains, or update the PR
objective to remove that requirement. If retaining the requirement, add cleanup
tied to the hook lifecycle and ensure both setTimeout and wrapperRaf work
scheduled through delayRef are cancelled on unmount.
- Line 48: 将 useDelayState 中的 window.setTimeout 调整为与 raf.ts 一致的裸 setTimeout
调用,避免在 SSR 或非浏览器环境依赖未定义的 window;保留现有延迟值和 setValue(nextValue) 行为不变。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5926d920-5698-4474-a489-c94ce1d29fee
📒 Files selected for processing (2)
src/hooks/useDelayState.tstests/useDelayState.test.tsx
💤 Files with no reviewable changes (1)
- tests/useDelayState.test.tsx
| delayRef.current = [ | ||
| false, | ||
| window.setTimeout(() => setValue(nextValue), immediatelyOrDelay.ms), | ||
| ]; | ||
| } else { | ||
| const frame = | ||
| typeof immediatelyOrDelay === 'object' | ||
| ? immediatelyOrDelay.frame | ||
| : undefined; | ||
| delayRef.current = [true, raf(() => setValue(nextValue), frame)]; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
PR 目标声称支持卸载清理,但实现中已移除清理逻辑
PR 目标明确提到 "cleanup on unmount",但根据上下文,卸载清理的 useEffect 已被移除。当组件卸载时仍有挂起的延迟任务,wrapperRaf 的多帧链({ frame: N })会在卸载后继续调度 N 个嵌套 setTimeout。虽然 React 19 中 setValue 对已卸载组件是空操作,但这些超时仍会持续触发,造成资源泄漏。
注意到此前评审明确要求移除清理 effect("no need cancel since setState will do nothing when unmount"),但这与 PR 目标中声明的 "cleanup on unmount" 存在矛盾。请确认是否需要更新 PR 目标描述,或重新加入卸载清理逻辑。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useDelayState.ts` around lines 46 - 55, Resolve the mismatch
between the PR’s “cleanup on unmount” requirement and the current useDelayState
implementation: either restore unmount cleanup that cancels pending delayRef
timers/raf chains, or update the PR objective to remove that requirement. If
retaining the requirement, add cleanup tied to the hook lifecycle and ensure
both setTimeout and wrapperRaf work scheduled through delayRef are cancelled on
unmount.

Summary
新增
useDelayStatehook:true时立即更新{ frame }和{ ms }指定延迟方式Testing
ut test --runInBandut tscut compileut lintSummary by CodeRabbit
useDelayState状态管理 Hook:可立即更新、默认在下一帧更新,或按指定毫秒延迟更新。useDelayState及配套类型。