Skip to content

Commit

Permalink
feat: Add useSet hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel committed Nov 14, 2019
1 parent abb7eb8 commit 095b4de
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
- [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
- [`useList`](./docs/useList.md) ~and [`useUpsert`](./docs/useUpsert.md)~ — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w)
- [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161)
- [`useSet`](./docs/useSet.md) — tracks state of a Set. [![][img-demo]](https://codesandbox.io/s/bold-shtern-6jlgw)
- [`useQueue`](./docs/useQueue.md) — implements simple queue.
- [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)
- [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo)
Expand Down
24 changes: 24 additions & 0 deletions docs/useSet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `useSet`

React state hook that tracks a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).

## Usage

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

const Demo = () => {
const [set, { add, has, remove, reset }] = useSet(new Set(['hello']));

return (
<div>
<button onClick={() => add(String(Date.now()))}>Add</button>
<button onClick={() => reset()}>Reset</button>
<button onClick={() => remove('hello')} disabled={!has('hello')}>
Remove 'hello'
</button>
<pre>{JSON.stringify(Array.from(set), null, 2)}</pre>
</div>
);
};
```
23 changes: 23 additions & 0 deletions src/__stories__/useSet.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 { useSet } from '..';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [set, { add, has, remove, reset }] = useSet(new Set(['hello']));

return (
<div>
<button onClick={() => add(String(Date.now()))}>Add</button>
<button onClick={() => reset()}>Reset</button>
<button onClick={() => remove('hello')} disabled={!has('hello')}>
Remove 'hello'
</button>
<pre>{JSON.stringify(Array.from(set), null, 2)}</pre>
</div>
);
};

storiesOf('State|useSet', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useSet.md')} />)
.add('Demo', () => <Demo />);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ export { useMultiStateValidator } from './useMultiStateValidator';
export { default as useWindowScroll } from './useWindowScroll';
export { default as useWindowSize } from './useWindowSize';
export { default as useMeasure } from './useMeasure';
export { default as useSet } from './useSet';
26 changes: 26 additions & 0 deletions src/useSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState, useMemo } from 'react';

export interface Actions<K> {
has: (key: K) => boolean;
add: (key: K) => void;
remove: (key: K) => void;
reset: () => void;
}

const useSet = <K>(initialSet = new Set<K>()): [Set<K>, Actions<K>] => {
const [set, setSet] = useState(initialSet);

const utils = useMemo<Actions<K>>(
() => ({
has: item => set.has(item),
add: item => setSet(prevSet => new Set([...Array.from(prevSet), item])),
remove: item => setSet(prevSet => new Set(Array.from(prevSet).filter(i => i !== item))),
reset: () => setSet(initialSet),
}),
[setSet]
);

return [set, utils];
};

export default useSet;
121 changes: 121 additions & 0 deletions tests/useSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useSet from '../src/useSet';

const setUp = <K>(initialSet?: Set<K>) => renderHook(() => useSet(initialSet));

it('should init set and utils', () => {
const { result } = setUp(new Set([1, 2]));
const [set, utils] = result.current;

expect(set).toEqual(new Set([1, 2]));
expect(utils).toStrictEqual({
has: expect.any(Function),
add: expect.any(Function),
remove: expect.any(Function),
reset: expect.any(Function),
});
});

it('should init empty set if no initial set provided', () => {
const { result } = setUp();

expect(result.current[0]).toEqual(new Set());
});

it('should have an initially provided key', () => {
const { result } = setUp(new Set(['a']));
const [, utils] = result.current;

let value;
act(() => {
value = utils.has('a');
});

expect(value).toBe(true);
});

it('should get false for non-existing key', () => {
const { result } = setUp(new Set(['a']));
const [, utils] = result.current;

let value;
act(() => {
value = utils.has('nonExisting');
});

expect(value).toBe(false);
});

it('should add a new key', () => {
const { result } = setUp(new Set(['oldKey']));
const [, utils] = result.current;

act(() => {
utils.add('newKey');
});

expect(result.current[0]).toEqual(new Set(['oldKey', 'newKey']));
});

it('should work if setting existing key', () => {
const { result } = setUp(new Set(['oldKey']));
const [, utils] = result.current;

act(() => {
utils.add('oldKey');
});

expect(result.current[0]).toEqual(new Set(['oldKey']));
});

it('should remove existing key', () => {
const { result } = setUp(new Set([1, 2]));
const [, utils] = result.current;

act(() => {
utils.remove(2);
});

expect(result.current[0]).toEqual(new Set([1]));
});

it('should do nothing if removing non-existing key', () => {
const { result } = setUp(new Set(['a', 'b']));
const [, utils] = result.current;

act(() => {
utils.remove('nonExisting');
});

expect(result.current[0]).toEqual(new Set(['a', 'b']));
});

it('should reset to initial set provided', () => {
const { result } = setUp(new Set([1]));
const [, utils] = result.current;

act(() => {
utils.add(2);
});

expect(result.current[0]).toEqual(new Set([1, 2]));

act(() => {
utils.reset();
});

expect(result.current[0]).toEqual(new Set([1]));
});

it('should memoized its utils methods', () => {
const { result } = setUp(new Set(['a', 'b']));
const [, utils] = result.current;
const { add } = utils;

act(() => {
add('foo');
});

expect(result.current[1]).toBe(utils);
expect(result.current[1].add).toBe(add);
});

0 comments on commit 095b4de

Please sign in to comment.