Skip to content

Commit b5a95d1

Browse files
committed
feat: add useChanging for change detection
close #113
1 parent 47e4758 commit b5a95d1

File tree

5 files changed

+114
-0
lines changed

5 files changed

+114
-0
lines changed

packages/state-hooks/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,33 @@ import { usePrevious, useUndoable } from 'state-hooks';
2929

3030
#### Table of Contents
3131

32+
- [useChanging](#usechanging)
3233
- [usePrevious](#useprevious)
3334
- [useTimeline](#usetimeline)
3435
- [useToggle](#usetoggle)
3536
- [useUndoable](#useundoable)
3637

38+
### useChanging
39+
40+
Tracks whether a value has changed over a relatively given period of time.
41+
42+
#### Parameters
43+
44+
- `value` **T** Props, state or any other calculated value.
45+
- `groupingIntervalMs` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Time interval, in milliseconds, to group a batch of changes by. (optional, default `150`)
46+
47+
#### Examples
48+
49+
```javascript
50+
function Component() {
51+
const scrollCoords = useWindowScrollCoords();
52+
const isScrolling = useChanging(scrollCoords);
53+
// ...
54+
}
55+
```
56+
57+
Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` if the value has changed at least once over the given interval, or `false` otherwise.
58+
3759
### usePrevious
3860

3961
Tracks previous state of a value.

packages/state-hooks/documentation.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
toc:
2+
- useChanging
23
- usePrevious
34
- useTimeline
45
- useToggle

packages/state-hooks/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { default as useChanging } from './useChanging';
12
export { default as usePrevious } from './usePrevious';
23
export { default as useTimeline } from './useTimeline';
34
export { default as useToggle } from './useToggle';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import { useChanging } from '.';
3+
4+
jest.useFakeTimers();
5+
6+
test('detect changes of a value over time', () => {
7+
const { result, rerender } = renderHook(({ value }) => useChanging(value), {
8+
initialProps: { value: 0 },
9+
});
10+
expect(result.current).toBe(false);
11+
12+
rerender({ value: 1 });
13+
expect(result.current).toBe(true);
14+
15+
act(() => {
16+
jest.advanceTimersByTime(100);
17+
});
18+
expect(result.current).toBe(true);
19+
20+
act(() => {
21+
jest.advanceTimersByTime(100);
22+
});
23+
expect(result.current).toBe(false);
24+
25+
rerender({ value: 2 });
26+
expect(result.current).toBe(true);
27+
});
28+
29+
test('handle changing grouping interval', () => {
30+
const { result, rerender } = renderHook(
31+
({ value, groupingIntervalMs }) => useChanging(value, groupingIntervalMs),
32+
{ initialProps: { value: 0, groupingIntervalMs: 500 } },
33+
);
34+
35+
rerender({ value: 1, groupingIntervalMs: 500 });
36+
act(() => {
37+
jest.advanceTimersByTime(300);
38+
});
39+
expect(result.current).toBe(true);
40+
41+
rerender({ value: 1, groupingIntervalMs: 1000 });
42+
act(() => {
43+
jest.advanceTimersByTime(700);
44+
});
45+
expect(result.current).toBe(true);
46+
47+
act(() => {
48+
jest.advanceTimersByTime(300);
49+
});
50+
expect(result.current).toBe(false);
51+
});
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
/**
4+
* Tracks whether a value has changed over a relatively given period of time.
5+
*
6+
* @param value Props, state or any other calculated value.
7+
* @param {number} groupingIntervalMs Time interval, in milliseconds, to group a batch of changes by.
8+
* @returns `true` if the value has changed at least once over the given interval, or `false` otherwise.
9+
*
10+
* @example
11+
* function Component() {
12+
* const scrollCoords = useWindowScrollCoords();
13+
* const isScrolling = useChanging(scrollCoords);
14+
* // ...
15+
* }
16+
*/
17+
export default function useChanging<T>(
18+
value: T,
19+
groupingIntervalMs = 150,
20+
): boolean {
21+
const [isChanging, setChanging] = useState(false);
22+
const prevGroupingIntervalMsRef = useRef(0);
23+
24+
useEffect(() => {
25+
// Prevent initial state from being true
26+
if (groupingIntervalMs !== prevGroupingIntervalMsRef.current) {
27+
prevGroupingIntervalMsRef.current = groupingIntervalMs;
28+
} else {
29+
setChanging(true);
30+
}
31+
32+
const timeoutID = setTimeout(() => setChanging(false), groupingIntervalMs);
33+
return () => {
34+
clearTimeout(timeoutID);
35+
};
36+
}, [groupingIntervalMs, value]);
37+
38+
return isChanging;
39+
}

0 commit comments

Comments
 (0)