Skip to content

Commit aaa4b65

Browse files
committed
Simplify throttling using react-use
1 parent 20e8f75 commit aaa4b65

2 files changed

Lines changed: 28 additions & 137 deletions

File tree

src/hooks/useThrottledTrigger.js

Lines changed: 28 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,44 @@
1-
import { useCallback, useEffect, useMemo, useRef } from "react";
2-
3-
export function createThrottledTrigger({
4-
callback,
5-
throttleMs,
6-
now = Date.now,
7-
setTimer = setTimeout,
8-
clearTimer = clearTimeout,
9-
}) {
10-
const state = {
11-
timer: null,
12-
lastRunAt: null,
13-
};
14-
15-
const invoke = () => {
16-
state.lastRunAt = now();
17-
callback?.();
18-
};
19-
20-
const schedule = (waitMs) => {
21-
state.timer = setTimer(() => {
22-
state.timer = null;
23-
invoke();
24-
}, waitMs);
25-
};
26-
27-
const runNow = () => {
28-
if (state.timer != null) {
29-
clearTimer(state.timer);
30-
state.timer = null;
31-
}
32-
invoke();
33-
};
34-
35-
const trigger = () => {
36-
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
37-
invoke();
38-
return;
39-
}
40-
41-
if (state.timer != null) return;
42-
43-
if (state.lastRunAt == null) {
44-
invoke();
45-
return;
46-
}
47-
48-
const elapsed = now() - state.lastRunAt;
49-
if (elapsed >= throttleMs) {
50-
invoke();
51-
return;
52-
}
53-
54-
schedule(throttleMs - elapsed);
55-
};
56-
57-
const dispose = () => {
58-
if (state.timer != null) {
59-
clearTimer(state.timer);
60-
state.timer = null;
61-
}
62-
};
63-
64-
return { trigger, runNow, dispose };
65-
}
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import useThrottleFn from "react-use/esm/useThrottleFn.js";
663

674
export function useThrottledTrigger({ callback, throttleMs = 150 }) {
685
const callbackRef = useRef(callback);
6+
const skipNextThrottledCallRef = useRef(false);
7+
const [triggerVersion, setTriggerVersion] = useState(0);
698

709
useEffect(() => {
7110
callbackRef.current = callback;
7211
}, [callback]);
7312

74-
const controller = useMemo(
75-
() =>
76-
createThrottledTrigger({
77-
callback: () => callbackRef.current?.(),
78-
throttleMs,
79-
}),
80-
[throttleMs],
13+
useThrottleFn(
14+
() => {
15+
if (skipNextThrottledCallRef.current) {
16+
skipNextThrottledCallRef.current = false;
17+
return;
18+
}
19+
callbackRef.current?.();
20+
},
21+
throttleMs,
22+
[triggerVersion],
8123
);
8224

83-
useEffect(() => () => controller.dispose(), [controller]);
84-
8525
const trigger = useCallback(() => {
86-
controller.trigger();
87-
}, [controller]);
26+
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
27+
callbackRef.current?.();
28+
return;
29+
}
30+
setTriggerVersion((version) => version + 1);
31+
}, [throttleMs]);
8832

8933
const runNow = useCallback(() => {
90-
controller.runNow();
91-
}, [controller]);
34+
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
35+
callbackRef.current?.();
36+
return;
37+
}
38+
skipNextThrottledCallRef.current = true;
39+
callbackRef.current?.();
40+
setTriggerVersion((version) => version + 1);
41+
}, [throttleMs]);
9242

9343
return { trigger, runNow };
9444
}

src/tests/useRandomScale.test.js

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,6 @@ import {
66
applyRandomizedScale,
77
formatRandomizedScaleAnnouncement,
88
} from "@/hooks/useRandomScale";
9-
import { createThrottledTrigger } from "@/hooks/useThrottledTrigger";
10-
11-
test("createThrottledTrigger throttles repeated trigger calls", () => {
12-
let nowMs = 0;
13-
let nextTimerId = 1;
14-
const activeTimers = new Map();
15-
const calls = [];
16-
17-
const setTimer = (fn, delay) => {
18-
const timerId = nextTimerId;
19-
nextTimerId += 1;
20-
activeTimers.set(timerId, { fn, dueAt: nowMs + delay });
21-
return timerId;
22-
};
23-
24-
const clearTimer = (timerId) => {
25-
activeTimers.delete(timerId);
26-
};
27-
28-
const runTimersDue = () => {
29-
const due = [...activeTimers.entries()]
30-
.filter(([, timer]) => timer.dueAt <= nowMs)
31-
.sort((a, b) => a[1].dueAt - b[1].dueAt);
32-
33-
due.forEach(([timerId, timer]) => {
34-
activeTimers.delete(timerId);
35-
timer.fn();
36-
});
37-
};
38-
39-
const throttled = createThrottledTrigger({
40-
callback: () => calls.push(nowMs),
41-
throttleMs: 100,
42-
now: () => nowMs,
43-
setTimer,
44-
clearTimer,
45-
});
46-
47-
throttled.trigger();
48-
assert.deepEqual(calls, [0]);
49-
50-
nowMs = 20;
51-
throttled.trigger();
52-
throttled.trigger();
53-
assert.equal(activeTimers.size, 1);
54-
assert.deepEqual(calls, [0]);
55-
56-
nowMs = 99;
57-
runTimersDue();
58-
assert.deepEqual(calls, [0]);
59-
60-
nowMs = 100;
61-
runTimersDue();
62-
assert.deepEqual(calls, [0, 100]);
63-
64-
nowMs = 130;
65-
throttled.runNow();
66-
assert.deepEqual(calls, [0, 100, 130]);
67-
});
689

6910
test("applyRandomizedScale updates state by randomize mode", () => {
7011
const result = { root: "D", scale: "Dorian" };

0 commit comments

Comments
 (0)