Skip to content

feat: add useDelayState hook#796

Merged
zombieJ merged 5 commits into
masterfrom
feat/use-delay-state
Jul 13, 2026
Merged

feat: add useDelayState hook#796
zombieJ merged 5 commits into
masterfrom
feat/use-delay-state

Conversation

@zombieJ

@zombieJ zombieJ commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

新增 useDelayState hook:

  • 默认延迟一帧更新,传入 true 时立即更新
  • 支持 { frame }{ ms } 指定延迟方式
  • 所有 pending 更新遵循 last-call-wins
  • 支持 value、functional updater 和卸载清理
  • 从包根入口导出 hook 及相关类型

Testing

  • ut test --runInBand
  • ut tsc
  • ut compile
  • ut lint

Summary by CodeRabbit

  • 新功能
    • 新增 useDelayState 状态管理 Hook:可立即更新、默认在下一帧更新,或按指定毫秒延迟更新。
    • 支持按帧延迟、函数式更新,并自动以最新一次挂起更新为准。
    • 通过公共 API 导出 useDelayState 及配套类型。
  • 测试
    • 新增用例覆盖默认下一帧、立即生效、毫秒/帧延迟、多次更新仅应用最新、以及函数式更新逻辑。

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
util Ready Ready Preview, Comment Jul 13, 2026 9:54am

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

React Doctor found no issues. 🎉

⚠️ Warning: .github/workflows/react-doctor.yml is configured incorrectly. See below to fix.

React Doctor compares against master to report only the issues this pull request introduces. This run couldn't complete that comparison (usually a shallow CI checkout with no merge base), so it listed every issue in the changed files, including ones that already existed on master.

Add fetch-depth: 0 to the actions/checkout step in .github/workflows/react-doctor.yml so the checkout includes the history React Doctor needs:

 jobs:
   react-doctor:
     steps:
       - uses: actions/checkout@v5
+        with:
+          fetch-depth: 0

       - uses: millionco/react-doctor@v2

To silence this warning, set silence-missing-baseline-warning: true on the React Doctor action.

Reviewed by React Doctor for commit 1ab58e5.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/hooks/useDelayState.ts Outdated
Comment on lines +22 to +30
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;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

问题分析

  1. TypeScript 类型只读问题:在 TypeScript 中,使用 React.useRef<T>(null) 会被推导为 RefObject<T>,其 current 属性是只读的(readonly current: T | null)。因此,直接对 rafRef.currenttimeoutRef.current 进行赋值(如第 28、29 行)在严格模式或标准 React 类型定义下会触发编译错误。为了使其可变,应当将类型声明为 React.useRef<T | null>(null),这样会被推导为 MutableRefObject
  2. 非空断言(!)的安全隐患:在 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;
    }
  });

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

✅ Preview is ready!

PR preview ✅ Ready ✅ Ready
🔗 Preview https://react-component-util-preview-pr-796.surge.sh
📝 Commit1ab58e5
⏱️ Build time34.522s
📦 Size1.7 MB (-7 B ⬇️) · 45 files
🪵 LogsView logs
📱 MobileScan to open preview on mobile

↩️ Previous: ⚡️ 1ab58e5 · react-component-util-preview-pr-796.surge.sh (open ↗) · 2026-07-13 09:55:16 UTC

🤖 Powered by surge-preview

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.72%. Comparing base (27a3007) to head (1ab58e5).
⚠️ Report is 1 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zombieJ, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 654cf8b7-550b-48ce-b323-018ddfb8b2d5

📥 Commits

Reviewing files that changed from the base of the PR and between d1486a0 and 1ab58e5.

📒 Files selected for processing (1)
  • src/hooks/useDelayState.ts

Walkthrough

新增 useDelayState React Hook,支持立即更新、下一帧更新及指定毫秒或帧数延迟,并取消旧的挂起任务。入口文件导出 Hook 与相关类型,测试覆盖各种更新时序和最新更新覆盖行为。

Changes

延迟状态更新

Layer / File(s) Summary
Hook 契约与实现
src/hooks/useDelayState.ts, src/index.ts
定义 DelayConfigSetDelayState 类型,实现立即、按帧和按毫秒更新,取消旧任务并新增入口导出。
行为验证
tests/useDelayState.test.tsx
验证默认帧延迟、立即更新、显式延迟、最新更新覆盖及函数式更新。

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
Loading

Poem

我是小兔,蹦过下一帧,
延迟状态稳稳更新。
新任务来,旧任务退场,
立即写入也不慌。
代码随时间轻轻发光。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次变更的核心:新增 useDelayState hook。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/use-delay-state

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/hooks/useDelayState.ts Outdated
): [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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

两个 ref 有点多余,你可以直接 useRef<[isRaf: boolean, delay: number]> // delay when is raf means delay raf times, or ms will be delay ms

Comment thread src/hooks/useDelayState.ts Outdated
},
);

React.useEffect(() => cancelPending, [cancelPending]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need cancel since setState will do nothing when unmount

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just remove this effect hooks

Comment thread src/hooks/useDelayState.ts Outdated
Comment thread src/hooks/useDelayState.ts Outdated
} else if ('ms' in delayConfig) {
delayRef.current = [
false,
window.setTimeout(() => setValue(nextValue), delayConfig.ms),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不要使用 window,你如果定义搞不定。直接使用 typeof setTimeout

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between dd526d0 and d1486a0.

📒 Files selected for processing (2)
  • src/hooks/useDelayState.ts
  • tests/useDelayState.test.tsx
💤 Files with no reviewable changes (1)
  • tests/useDelayState.test.tsx

Comment thread src/hooks/useDelayState.ts Outdated
Comment on lines +46 to +55
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)];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment thread src/hooks/useDelayState.ts Outdated
@zombieJ zombieJ merged commit 7c5e30e into master Jul 13, 2026
12 of 13 checks passed
@zombieJ zombieJ deleted the feat/use-delay-state branch July 13, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant