Skip to content

Commit 0c077f8

Browse files
Create useThrottledState (#841)
* Create useThrottledState * use defaultThrottleSettings * fix failing test * remove function condition --------- Co-authored-by: Adhitya Mamallan <[email protected]>
1 parent 8afcf00 commit 0c077f8

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { renderHook, act, waitFor } from '@testing-library/react';
2+
import throttle from 'lodash/throttle';
3+
4+
import useThrottledState from '../use-throttled-state';
5+
6+
jest.mock('lodash/throttle', () => {
7+
const original = jest.requireActual('lodash/throttle');
8+
return jest.fn(original);
9+
});
10+
const mockedThrottle = throttle as jest.Mock;
11+
12+
const defaultThrottleSettings = { leading: false, trailing: true };
13+
14+
describe('useThrottledState', () => {
15+
beforeEach(() => {
16+
jest.useFakeTimers();
17+
mockedThrottle.mockClear();
18+
});
19+
20+
afterEach(() => {
21+
jest.useRealTimers();
22+
});
23+
24+
it('should initialize with the given value', () => {
25+
const { result } = renderHook(() => useThrottledState(0));
26+
expect(result.current[0]).toBe(0);
27+
});
28+
29+
it('should pass the correct settings to throttle', () => {
30+
renderHook(() => useThrottledState(0, 1, defaultThrottleSettings));
31+
expect(mockedThrottle).toHaveBeenCalledWith(
32+
expect.any(Function),
33+
1,
34+
defaultThrottleSettings
35+
);
36+
});
37+
38+
it('should immediately update if executeImmediately is true', () => {
39+
const { result } = renderHook(() =>
40+
useThrottledState(0, 10000, defaultThrottleSettings)
41+
);
42+
const setState = result.current[1];
43+
44+
act(() => {
45+
setState((prev) => prev + 1, true);
46+
});
47+
48+
expect(result.current[0]).toBe(1);
49+
});
50+
51+
it('should return the latest value if rerendered externaly', async () => {
52+
const { result, rerender } = renderHook(() =>
53+
useThrottledState(0, 10000, defaultThrottleSettings)
54+
);
55+
const setState = result.current[1];
56+
57+
act(() => {
58+
setState((prev) => prev + 1);
59+
setState((prev) => prev + 1);
60+
setState((prev) => prev + 1);
61+
});
62+
63+
expect(result.current[0]).toBe(0);
64+
rerender();
65+
expect(result.current[0]).toBe(3);
66+
});
67+
68+
it('should throttle updating state', async () => {
69+
const { result } = renderHook(() =>
70+
useThrottledState(0, 10000, defaultThrottleSettings)
71+
);
72+
const setState = result.current[1];
73+
74+
act(() => {
75+
setState((prev) => prev + 1);
76+
setState((prev) => prev + 1);
77+
setState((prev) => prev + 1);
78+
});
79+
80+
expect(result.current[0]).toBe(0);
81+
jest.advanceTimersByTime(10000);
82+
await waitFor(() => expect(result.current[0]).toBe(3));
83+
});
84+
});

src/hooks/use-throttled-state.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useCallback, useState, useRef, useMemo, useEffect } from 'react';
2+
3+
import type { ThrottleSettings } from 'lodash';
4+
import throttle from 'lodash/throttle';
5+
6+
export default function useThrottledState<State>(
7+
initValue: State,
8+
throttleMillis = 700,
9+
throttleOptions: ThrottleSettings = {}
10+
) {
11+
const { leading, trailing } = throttleOptions;
12+
const stateRef = useRef(initValue);
13+
const [, setState] = useState<State>(initValue);
14+
15+
const throttledRerender = useMemo(
16+
() => throttle(setState, throttleMillis, { leading, trailing }),
17+
[setState, throttleMillis, leading, trailing]
18+
);
19+
20+
// clear previous throttled events
21+
useEffect(() => {
22+
return () => throttledRerender.cancel();
23+
}, [throttledRerender]);
24+
25+
const refSetState = useCallback(
26+
(callback: (arg: State) => State, executeImmediately?: boolean): void => {
27+
const newVal = callback(stateRef.current);
28+
stateRef.current = newVal;
29+
throttledRerender(newVal);
30+
if (executeImmediately) {
31+
throttledRerender.flush();
32+
}
33+
},
34+
[throttledRerender]
35+
);
36+
37+
return [stateRef.current, refSetState] as const;
38+
}

0 commit comments

Comments
 (0)