Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add usePreviousDistinct #551

Merged
merged 4 commits into from
Aug 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -54,6 +54,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;
}