Skip to content

Commit 292b965

Browse files
devmgnclaude
andcommitted
refactor(debounce): rename baselineTime, unify wait, document stability
- Rename `lastInvokeTime` → `baselineTime` to reflect its actual role as the maxWait baseline - Replace sentinel `0` with `undefined` so `Date.now() === 0` edge cases do not misfire - Unify parameter name `debounceMs` → `wait` across `debounce()` (matches lodash/es-toolkit and `useDebouncedCallback`) - Add JSDoc to `useDebouncedCallback` warning about passing stable references for `callback` / `options` No behavior change. 92/92 tests pass, `pnpm check` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent b84b6a0 commit 292b965

2 files changed

Lines changed: 20 additions & 13 deletions

File tree

src/hooks/useDebouncedCallback/useDebouncedCallback.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import type { DebounceOptions } from "../../utils/debounce";
22
import { useEffect, useMemo } from "react";
33
import { debounce } from "../../utils/debounce";
44

5+
/**
6+
* `callback` と `options` は安定参照で渡すこと。毎レンダーで新しい参照を渡すと
7+
* `useMemo` が再計算され、保留中のタイマーと leading クールダウン状態が失われる。
8+
* `options` が静的な場合はコンポーネント外か `useMemo` で固定する。
9+
*/
510
export function useDebouncedCallback<Args extends unknown[]>(
611
callback: (...args: Args) => void,
712
wait: number,

src/utils/debounce/debounce.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export interface DebounceOptions {
33
edges?: Array<"leading" | "trailing">;
44
/**
55
* 連続呼び出し中でも最低 `maxWait` ms 以内に必ず `func` が実行されることを保証する。
6-
* `debounceMs` 未満の値は `debounceMs` にクランプされる。
6+
* `wait` 未満の値は `wait` にクランプされる。
77
*/
88
maxWait?: number;
99
}
@@ -32,10 +32,10 @@ function assertNonNegativeFinite(name: string, value: number): void {
3232

3333
export function debounce<Args extends unknown[]>(
3434
func: (...args: Args) => void,
35-
debounceMs: number,
35+
wait: number,
3636
options?: DebounceOptions,
3737
): DebouncedFunction<Args> {
38-
assertNonNegativeFinite("debounceMs", debounceMs);
38+
assertNonNegativeFinite("wait", wait);
3939

4040
const edges = options?.edges ?? ["trailing"];
4141
const hasLeading = edges.includes("leading");
@@ -45,14 +45,16 @@ export function debounce<Args extends unknown[]>(
4545
if (rawMaxWait !== undefined) {
4646
assertNonNegativeFinite("maxWait", rawMaxWait);
4747
}
48-
// lodash 互換: maxWait < debounceMsdebounceMs にクランプ
48+
// lodash 互換: maxWait < waitwait にクランプ
4949
const maxWait =
50-
rawMaxWait === undefined ? undefined : Math.max(rawMaxWait, debounceMs);
50+
rawMaxWait === undefined ? undefined : Math.max(rawMaxWait, wait);
5151

5252
let timerId: ReturnType<typeof setTimeout> | undefined = undefined;
5353
let lastArgs: Args | undefined = undefined;
5454
let lastThis: unknown = undefined;
55-
let lastInvokeTime = 0;
55+
// maxWait の経過判定に使う基準時刻。
56+
// 新規バースト開始時と invoke 時に `Date.now()` で更新する。
57+
let baselineTime: number | undefined = undefined;
5658
let isLeadingInvoked = false;
5759
let isAborted = options?.signal?.aborted ?? false;
5860

@@ -64,7 +66,7 @@ export function debounce<Args extends unknown[]>(
6466
// 設定された lastArgs/lastThis を上書きしない
6567
lastArgs = undefined;
6668
lastThis = undefined;
67-
lastInvokeTime = Date.now();
69+
baselineTime = Date.now();
6870
Reflect.apply(func, thisArg, args);
6971
}
7072
}
@@ -76,7 +78,7 @@ export function debounce<Args extends unknown[]>(
7678
}
7779
lastArgs = undefined;
7880
lastThis = undefined;
79-
lastInvokeTime = 0;
81+
baselineTime = undefined;
8082
isLeadingInvoked = false;
8183
}
8284

@@ -101,10 +103,10 @@ export function debounce<Args extends unknown[]>(
101103
clearTimeout(timerId);
102104
}
103105

104-
let delay = debounceMs;
106+
let delay = wait;
105107
let maxWaitForced = false;
106-
if (maxWait !== undefined && lastInvokeTime !== 0) {
107-
const maxRemaining = maxWait - (Date.now() - lastInvokeTime);
108+
if (maxWait !== undefined && baselineTime !== undefined) {
109+
const maxRemaining = maxWait - (Date.now() - baselineTime);
108110
if (maxRemaining <= 0) {
109111
delay = 0;
110112
maxWaitForced = true;
@@ -142,9 +144,9 @@ export function debounce<Args extends unknown[]>(
142144

143145
// 新しいバーストの開始: maxWait のベースラインを更新
144146
// (前回の invoke から時間が経った後の呼び出しが誤って maxWait 強制発火扱いに
145-
// なるのを防ぐため、毎バーストで lastInvokeTime を再設定する)
147+
// なるのを防ぐため、毎バーストで baselineTime を再設定する)
146148
if (timerId === undefined) {
147-
lastInvokeTime = Date.now();
149+
baselineTime = Date.now();
148150
}
149151

150152
if (hasLeading && !isLeadingInvoked) {

0 commit comments

Comments
 (0)