Skip to content

Commit

Permalink
feat: add usePreviousDistinct (#551)
Browse files Browse the repository at this point in the history
* feat: add usePreviousDistinct

* Cleanup

* Added storybook docs

* improve demo in docs
  • Loading branch information
Paul Sachs authored and Belco90 committed Aug 25, 2019
1 parent c5151c7 commit 6c3e569
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 0 deletions.
51 changes: 51 additions & 0 deletions docs/usePreviousDistinct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# `usePreviousDistinct`

Just like `usePrevious` but it will only update once the value actually changes. This is important when other
hooks are involved and you aren't just interested in the previous props version, but want to know the previous
distinct value

## Usage

```jsx
import {usePreviousDistinct, useCounter} from 'react-use';

const Demo = () => {
const [count, { inc: relatedInc }] = useCounter(0);
const [unrelatedCount, { inc }] = useCounter(0);
const prevCount = usePreviousDistinct(count);

return (
<p>
Now: {count}, before: {prevCount}
<button onClick={() => relatedInc()}>Increment</button>
Unrelated: {unrelatedCount}
<button onClick={() => inc()}>Increment Unrelated</button>
</p>
);
};
```

You can also provide a way of identifying the value as unique. By default, a strict equals is used.

```jsx
import {usePreviousDistinct} from 'react-use';

const Demo = () => {
const [str, setStr] = React.useState("something_lowercase");
const [unrelatedCount] = React.useState(0);
const prevStr = usePreviousDistinct(str, (prev, next) => (prev && prev.toUpperCase()) === next.toUpperCase());

return (
<p>
Now: {count}, before: {prevCount}
Unrelated: {unrelatedCount}
</p>
);
};
```

## Reference

```ts
const prevState = usePreviousDistinct = <T>(state: T, compare?: (prev: T | undefined, next: T) => boolean): T;
```
23 changes: 23 additions & 0 deletions src/__stories__/usePreviousDistinct.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { usePreviousDistinct, useCounter } from '..';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [count, { inc: relatedInc }] = useCounter(0);
const [unrelatedCount, { inc }] = useCounter(0);
const prevCount = usePreviousDistinct(count);

return (
<p>
Now: {count}, before: {prevCount}
<button onClick={() => relatedInc()}>Increment</button>
Unrelated: {unrelatedCount}
<button onClick={() => inc()}>Increment Unrelated</button>
</p>
);
};

storiesOf('State|usePreviousDistinct', module)
.add('Docs', () => <ShowDocs md={require('../../docs/usePreviousDistinct.md')} />)
.add('Demo', () => <Demo />);
65 changes: 65 additions & 0 deletions src/__tests__/usePreviousDistinct.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { renderHook } from '@testing-library/react-hooks';
import usePreviousDistinct from '../usePreviousDistinct';

describe('usePreviousDistinct with default compare', () => {
const hook = renderHook(props => usePreviousDistinct(props), { initialProps: 0 });

it('should return undefined on initial render', () => {
expect(hook.result.current).toBe(undefined);
});

it('should return previous state only after a different value is rendered', () => {
expect(hook.result.current).toBeUndefined();
hook.rerender(1);
expect(hook.result.current).toBe(0);
hook.rerender(2);
hook.rerender(2);
expect(hook.result.current).toBe(1);

hook.rerender(3);
expect(hook.result.current).toBe(2);
});
});

describe('usePreviousDistinct with complex comparison', () => {
const exampleObjects = [
{
id: 'something-unique',
name: 'Nancy',
},
{
id: 'something-unique2',
name: 'Fred',
},
{
id: 'something-unique3',
name: 'Bill',
},
{
id: 'something-unique4',
name: 'Alice',
},
];
const hook = renderHook(
props => usePreviousDistinct(props, (prev, next) => (prev && prev.id) === (next && next.id)),
{
initialProps: exampleObjects[0],
}
);

it('should return undefined on initial render', () => {
expect(hook.result.current).toBe(undefined);
});

it('should return previous state only after a different value is rendered', () => {
expect(hook.result.current).toBeUndefined();
hook.rerender(exampleObjects[1]);
expect(hook.result.current).toMatchObject(exampleObjects[0]);
hook.rerender(exampleObjects[2]);
hook.rerender(exampleObjects[2]);
expect(hook.result.current).toMatchObject(exampleObjects[1]);

hook.rerender(exampleObjects[3]);
expect(hook.result.current).toMatchObject(exampleObjects[2]);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export { default as useOrientation } from './useOrientation';
export { default as usePageLeave } from './usePageLeave';
export { default as usePermission } from './usePermission';
export { default as usePrevious } from './usePrevious';
export { default as usePreviousDistinct } from './usePreviousDistinct';
export { default as usePromise } from './usePromise';
export { default as useRaf } from './useRaf';
export { default as useRafLoop } from './useRafLoop';
Expand Down
19 changes: 19 additions & 0 deletions src/usePreviousDistinct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useRef } from 'react';

function strictEquals<T>(prev: T | undefined, next: T) {
return prev === next;
}

export default function usePreviousDistinct<T>(
value: T,
compare: (prev: T | undefined, next: T) => boolean = strictEquals
) {
const prevRef = useRef<T>();
const curRef = useRef<T>();
if (!compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}

return prevRef.current;
}

0 comments on commit 6c3e569

Please sign in to comment.