Skip to content

Commit 6c3e569

Browse files
Paul SachsBelco90
Paul Sachs
authored andcommitted
feat: add usePreviousDistinct (#551)
* feat: add usePreviousDistinct * Cleanup * Added storybook docs * improve demo in docs
1 parent c5151c7 commit 6c3e569

File tree

5 files changed

+159
-0
lines changed

5 files changed

+159
-0
lines changed

Diff for: docs/usePreviousDistinct.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# `usePreviousDistinct`
2+
3+
Just like `usePrevious` but it will only update once the value actually changes. This is important when other
4+
hooks are involved and you aren't just interested in the previous props version, but want to know the previous
5+
distinct value
6+
7+
## Usage
8+
9+
```jsx
10+
import {usePreviousDistinct, useCounter} from 'react-use';
11+
12+
const Demo = () => {
13+
const [count, { inc: relatedInc }] = useCounter(0);
14+
const [unrelatedCount, { inc }] = useCounter(0);
15+
const prevCount = usePreviousDistinct(count);
16+
17+
return (
18+
<p>
19+
Now: {count}, before: {prevCount}
20+
<button onClick={() => relatedInc()}>Increment</button>
21+
Unrelated: {unrelatedCount}
22+
<button onClick={() => inc()}>Increment Unrelated</button>
23+
</p>
24+
);
25+
};
26+
```
27+
28+
You can also provide a way of identifying the value as unique. By default, a strict equals is used.
29+
30+
```jsx
31+
import {usePreviousDistinct} from 'react-use';
32+
33+
const Demo = () => {
34+
const [str, setStr] = React.useState("something_lowercase");
35+
const [unrelatedCount] = React.useState(0);
36+
const prevStr = usePreviousDistinct(str, (prev, next) => (prev && prev.toUpperCase()) === next.toUpperCase());
37+
38+
return (
39+
<p>
40+
Now: {count}, before: {prevCount}
41+
Unrelated: {unrelatedCount}
42+
</p>
43+
);
44+
};
45+
```
46+
47+
## Reference
48+
49+
```ts
50+
const prevState = usePreviousDistinct = <T>(state: T, compare?: (prev: T | undefined, next: T) => boolean): T;
51+
```

Diff for: src/__stories__/usePreviousDistinct.story.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { storiesOf } from '@storybook/react';
2+
import * as React from 'react';
3+
import { usePreviousDistinct, useCounter } from '..';
4+
import ShowDocs from './util/ShowDocs';
5+
6+
const Demo = () => {
7+
const [count, { inc: relatedInc }] = useCounter(0);
8+
const [unrelatedCount, { inc }] = useCounter(0);
9+
const prevCount = usePreviousDistinct(count);
10+
11+
return (
12+
<p>
13+
Now: {count}, before: {prevCount}
14+
<button onClick={() => relatedInc()}>Increment</button>
15+
Unrelated: {unrelatedCount}
16+
<button onClick={() => inc()}>Increment Unrelated</button>
17+
</p>
18+
);
19+
};
20+
21+
storiesOf('State|usePreviousDistinct', module)
22+
.add('Docs', () => <ShowDocs md={require('../../docs/usePreviousDistinct.md')} />)
23+
.add('Demo', () => <Demo />);

Diff for: src/__tests__/usePreviousDistinct.test.tsx

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import usePreviousDistinct from '../usePreviousDistinct';
3+
4+
describe('usePreviousDistinct with default compare', () => {
5+
const hook = renderHook(props => usePreviousDistinct(props), { initialProps: 0 });
6+
7+
it('should return undefined on initial render', () => {
8+
expect(hook.result.current).toBe(undefined);
9+
});
10+
11+
it('should return previous state only after a different value is rendered', () => {
12+
expect(hook.result.current).toBeUndefined();
13+
hook.rerender(1);
14+
expect(hook.result.current).toBe(0);
15+
hook.rerender(2);
16+
hook.rerender(2);
17+
expect(hook.result.current).toBe(1);
18+
19+
hook.rerender(3);
20+
expect(hook.result.current).toBe(2);
21+
});
22+
});
23+
24+
describe('usePreviousDistinct with complex comparison', () => {
25+
const exampleObjects = [
26+
{
27+
id: 'something-unique',
28+
name: 'Nancy',
29+
},
30+
{
31+
id: 'something-unique2',
32+
name: 'Fred',
33+
},
34+
{
35+
id: 'something-unique3',
36+
name: 'Bill',
37+
},
38+
{
39+
id: 'something-unique4',
40+
name: 'Alice',
41+
},
42+
];
43+
const hook = renderHook(
44+
props => usePreviousDistinct(props, (prev, next) => (prev && prev.id) === (next && next.id)),
45+
{
46+
initialProps: exampleObjects[0],
47+
}
48+
);
49+
50+
it('should return undefined on initial render', () => {
51+
expect(hook.result.current).toBe(undefined);
52+
});
53+
54+
it('should return previous state only after a different value is rendered', () => {
55+
expect(hook.result.current).toBeUndefined();
56+
hook.rerender(exampleObjects[1]);
57+
expect(hook.result.current).toMatchObject(exampleObjects[0]);
58+
hook.rerender(exampleObjects[2]);
59+
hook.rerender(exampleObjects[2]);
60+
expect(hook.result.current).toMatchObject(exampleObjects[1]);
61+
62+
hook.rerender(exampleObjects[3]);
63+
expect(hook.result.current).toMatchObject(exampleObjects[2]);
64+
});
65+
});

Diff for: src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export { default as useOrientation } from './useOrientation';
5555
export { default as usePageLeave } from './usePageLeave';
5656
export { default as usePermission } from './usePermission';
5757
export { default as usePrevious } from './usePrevious';
58+
export { default as usePreviousDistinct } from './usePreviousDistinct';
5859
export { default as usePromise } from './usePromise';
5960
export { default as useRaf } from './useRaf';
6061
export { default as useRafLoop } from './useRafLoop';

Diff for: src/usePreviousDistinct.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useRef } from 'react';
2+
3+
function strictEquals<T>(prev: T | undefined, next: T) {
4+
return prev === next;
5+
}
6+
7+
export default function usePreviousDistinct<T>(
8+
value: T,
9+
compare: (prev: T | undefined, next: T) => boolean = strictEquals
10+
) {
11+
const prevRef = useRef<T>();
12+
const curRef = useRef<T>();
13+
if (!compare(curRef.current, value)) {
14+
prevRef.current = curRef.current;
15+
curRef.current = value;
16+
}
17+
18+
return prevRef.current;
19+
}

0 commit comments

Comments
 (0)