Skip to content

Commit 9604cce

Browse files
Copilotliuyib
andcommitted
refactor: extract duplicated rate-limiting code into factory functions
Co-authored-by: liuyib <38221479+liuyib@users.noreply.github.com>
1 parent be1f0be commit 9604cce

File tree

11 files changed

+169
-150
lines changed

11 files changed

+169
-150
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect, useState } from 'react';
2+
import type { DependencyList, EffectCallback } from 'react';
3+
import useUpdateEffect from '../useUpdateEffect';
4+
import type { RateLimitOptions } from './createRateLimitFn';
5+
6+
type noop = (...args: any[]) => any;
7+
8+
export function createRateLimitEffect<T extends noop>(
9+
useRateLimitFn: (
10+
fn: T,
11+
options?: RateLimitOptions,
12+
) => {
13+
run: T;
14+
cancel: () => void;
15+
flush: () => void;
16+
},
17+
) {
18+
return function useRateLimitEffect(
19+
effect: EffectCallback,
20+
deps?: DependencyList,
21+
options?: RateLimitOptions,
22+
) {
23+
const [flag, setFlag] = useState({});
24+
25+
const { run } = useRateLimitFn(
26+
(() => {
27+
setFlag({});
28+
}) as T,
29+
options,
30+
);
31+
32+
// biome-ignore lint/correctness/useExhaustiveDependencies: deps is intentionally passed through from the user
33+
useEffect(() => {
34+
return run();
35+
}, deps);
36+
37+
useUpdateEffect(effect, [flag]);
38+
};
39+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useMemo } from 'react';
2+
import useLatest from '../useLatest';
3+
import useUnmount from '../useUnmount';
4+
import { isFunction } from '../utils';
5+
import isDev from '../utils/isDev';
6+
7+
type noop = (...args: any[]) => any;
8+
9+
export interface RateLimitOptions {
10+
wait?: number;
11+
leading?: boolean;
12+
trailing?: boolean;
13+
maxWait?: number;
14+
}
15+
16+
export interface RateLimitFunction<T extends noop> {
17+
(...args: Parameters<T>): ReturnType<T>;
18+
cancel: () => void;
19+
flush: () => void;
20+
}
21+
22+
export function createRateLimitFn<T extends noop>(
23+
rateLimitFn: (
24+
func: (...args: Parameters<T>) => ReturnType<T>,
25+
wait: number,
26+
options?: RateLimitOptions,
27+
) => RateLimitFunction<T>,
28+
hookName: string,
29+
) {
30+
return function useRateLimitFn(fn: T, options?: RateLimitOptions) {
31+
if (isDev) {
32+
if (!isFunction(fn)) {
33+
console.error(`${hookName} expected parameter is a function, got ${typeof fn}`);
34+
}
35+
}
36+
37+
const fnRef = useLatest(fn);
38+
39+
const wait = options?.wait ?? 1000;
40+
41+
// Note: We intentionally use an empty dependency array here.
42+
// The rateLimitFn is created once and captures the latest fn via fnRef.current
43+
// eslint-disable-next-line react-hooks/exhaustive-deps
44+
const rateLimited = useMemo(
45+
() =>
46+
rateLimitFn(
47+
(...args: Parameters<T>): ReturnType<T> => {
48+
return fnRef.current(...args);
49+
},
50+
wait,
51+
options,
52+
),
53+
// biome-ignore lint/correctness/useExhaustiveDependencies: rateLimitFn is stable, fnRef updates are captured via .current
54+
[],
55+
);
56+
57+
useUnmount(() => {
58+
rateLimited.cancel();
59+
});
60+
61+
return {
62+
run: rateLimited,
63+
cancel: rateLimited.cancel,
64+
flush: rateLimited.flush,
65+
};
66+
};
67+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useEffect, useState } from 'react';
2+
import type { RateLimitOptions } from './createRateLimitFn';
3+
4+
type noop = (...args: any[]) => any;
5+
6+
export function createRateLimitValue<T extends noop>(
7+
useRateLimitFn: (
8+
fn: T,
9+
options?: RateLimitOptions,
10+
) => {
11+
run: T;
12+
cancel: () => void;
13+
flush: () => void;
14+
},
15+
) {
16+
return function useRateLimitValue<V>(value: V, options?: RateLimitOptions) {
17+
const [rateLimited, setRateLimited] = useState(value);
18+
19+
const { run } = useRateLimitFn(
20+
(() => {
21+
setRateLimited(value);
22+
}) as T,
23+
options,
24+
);
25+
26+
// biome-ignore lint/correctness/useExhaustiveDependencies: run is stable, we only want to trigger on value changes
27+
useEffect(() => {
28+
run();
29+
}, [value]);
30+
31+
return rateLimited;
32+
};
33+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { createRateLimitFn } from './createRateLimitFn';
2+
export type { RateLimitOptions, RateLimitFunction } from './createRateLimitFn';
3+
export { createRateLimitValue } from './createRateLimitValue';
4+
export { createRateLimitEffect } from './createRateLimitEffect';
Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
import { useEffect, useState } from 'react';
2-
import useDebounceFn from '../useDebounceFn';
31
import type { DebounceOptions } from './debounceOptions';
2+
import useDebounceFn from '../useDebounceFn';
3+
import { createRateLimitValue } from '../createRateLimitHooks';
44

5-
function useDebounce<T>(value: T, options?: DebounceOptions) {
6-
const [debounced, setDebounced] = useState(value);
7-
8-
const { run } = useDebounceFn(() => {
9-
setDebounced(value);
10-
}, options);
11-
12-
useEffect(() => {
13-
run();
14-
}, [value]);
15-
16-
return debounced;
17-
}
5+
const useDebounce = createRateLimitValue(useDebounceFn);
186

197
export default useDebounce;
8+
export type { DebounceOptions };
Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
1-
import { useEffect, useState } from 'react';
2-
import type { DependencyList, EffectCallback } from 'react';
31
import type { DebounceOptions } from '../useDebounce/debounceOptions';
42
import useDebounceFn from '../useDebounceFn';
5-
import useUpdateEffect from '../useUpdateEffect';
3+
import { createRateLimitEffect } from '../createRateLimitHooks';
64

7-
function useDebounceEffect(
8-
effect: EffectCallback,
9-
deps?: DependencyList,
10-
options?: DebounceOptions,
11-
) {
12-
const [flag, setFlag] = useState({});
13-
14-
const { run } = useDebounceFn(() => {
15-
setFlag({});
16-
}, options);
17-
18-
useEffect(() => {
19-
return run();
20-
}, deps);
21-
22-
useUpdateEffect(effect, [flag]);
23-
}
5+
const useDebounceEffect = createRateLimitEffect(useDebounceFn);
246

257
export default useDebounceEffect;
8+
export type { DebounceOptions };
Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,10 @@
11
import { debounce } from '../utils/lodash-polyfill';
2-
import { useMemo } from 'react';
32
import type { DebounceOptions } from '../useDebounce/debounceOptions';
4-
import useLatest from '../useLatest';
5-
import useUnmount from '../useUnmount';
6-
import { isFunction } from '../utils';
7-
import isDev from '../utils/isDev';
3+
import { createRateLimitFn } from '../createRateLimitHooks';
84

95
type noop = (...args: any[]) => any;
106

11-
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
12-
if (isDev) {
13-
if (!isFunction(fn)) {
14-
console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);
15-
}
16-
}
17-
18-
const fnRef = useLatest(fn);
19-
20-
const wait = options?.wait ?? 1000;
21-
22-
const debounced = useMemo(
23-
() =>
24-
debounce(
25-
(...args: Parameters<T>): ReturnType<T> => {
26-
return fnRef.current(...args);
27-
},
28-
wait,
29-
options,
30-
),
31-
[],
32-
);
33-
34-
useUnmount(() => {
35-
debounced.cancel();
36-
});
37-
38-
return {
39-
run: debounced,
40-
cancel: debounced.cancel,
41-
flush: debounced.flush,
42-
};
43-
}
7+
const useDebounceFn = createRateLimitFn<noop>(debounce, 'useDebounceFn');
448

459
export default useDebounceFn;
10+
export type { DebounceOptions };

packages/hooks/src/useInfiniteScroll/__tests__/index.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ describe('useInfiniteScroll', () => {
4141
beforeAll(() => {
4242
vi.useFakeTimers();
4343
// Mock requestAnimationFrame to execute callbacks immediately
44-
mockRaf = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback) => {
45-
cb(0);
46-
return 0;
47-
}) as ReturnType<typeof vi.spyOn>;
44+
mockRaf = vi
45+
.spyOn(window, 'requestAnimationFrame')
46+
.mockImplementation((cb: FrameRequestCallback) => {
47+
cb(0);
48+
return 0;
49+
}) as ReturnType<typeof vi.spyOn>;
4850
});
4951

5052
afterAll(() => {
Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
import { useEffect, useState } from 'react';
2-
import useThrottleFn from '../useThrottleFn';
31
import type { ThrottleOptions } from './throttleOptions';
2+
import useThrottleFn from '../useThrottleFn';
3+
import { createRateLimitValue } from '../createRateLimitHooks';
44

5-
function useThrottle<T>(value: T, options?: ThrottleOptions) {
6-
const [throttled, setThrottled] = useState(value);
7-
8-
const { run } = useThrottleFn(() => {
9-
setThrottled(value);
10-
}, options);
11-
12-
useEffect(() => {
13-
run();
14-
}, [value]);
15-
16-
return throttled;
17-
}
5+
const useThrottle = createRateLimitValue(useThrottleFn);
186

197
export default useThrottle;
8+
export type { ThrottleOptions };
Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
1-
import { useEffect, useState } from 'react';
2-
import type { DependencyList, EffectCallback } from 'react';
31
import type { ThrottleOptions } from '../useThrottle/throttleOptions';
42
import useThrottleFn from '../useThrottleFn';
5-
import useUpdateEffect from '../useUpdateEffect';
3+
import { createRateLimitEffect } from '../createRateLimitHooks';
64

7-
function useThrottleEffect(
8-
effect: EffectCallback,
9-
deps?: DependencyList,
10-
options?: ThrottleOptions,
11-
) {
12-
const [flag, setFlag] = useState({});
13-
14-
const { run } = useThrottleFn(() => {
15-
setFlag({});
16-
}, options);
17-
18-
useEffect(() => {
19-
return run();
20-
}, deps);
21-
22-
useUpdateEffect(effect, [flag]);
23-
}
5+
const useThrottleEffect = createRateLimitEffect(useThrottleFn);
246

257
export default useThrottleEffect;
8+
export type { ThrottleOptions };

0 commit comments

Comments
 (0)