Repository: streamich/react-use
Pull request #772 (2019-11-14 10:21:23)

feat: Add useSet hook
# Description
Add a `useSet` hook 

<!-- Please include a summary of the change along with relevant motivation and context. -->
## Type of change
<!-- Check all relevant options. -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [X] New feature _(non-breaking change which adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist
- [x] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [x] Perform a code self-review
- [x] Comment the code, particularly in hard-to-understand areas
- [x] Add documentation
- [x] Add hook's story at Storybook
- [x] Cover changes with tests
- [x] Ensure the test suite passes (`yarn test`)
- [x] Provide 100% tests coverage
- [x] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [x] Make sure types are fine (`yarn lint:types`).

<!-- If you can't check all the checkboxes right now - check what you can, create a Draft PR, make some changes if needed and get back to it when you will be able to put some marks in list. -->


daniel-hauser: Note: The demo in codesandbox won't work before this is merged

Patch:
diff --git a/README.md b/README.md
index 06364ec9..5b42e2e7 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,7 @@
   - [`useCounter` and `useNumber`](./docs/useCounter.md) &mdash; 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)~ &mdash; tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w)
   - [`useMap`](./docs/useMap.md) &mdash; tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161)
+  - [`useSet`](./docs/useSet.md) &mdash; tracks state of a Set. [![][img-demo]](https://codesandbox.io/s/bold-shtern-6jlgw)
   - [`useQueue`](./docs/useQueue.md) &mdash; implements simple queue.
   - [`useStateValidator`](./docs/useStateValidator.md) &mdash; tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)
   - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) &mdash; alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo)
diff --git a/docs/useSet.md b/docs/useSet.md
new file mode 100644
index 00000000..8b6f620a
--- /dev/null
+++ b/docs/useSet.md
@@ -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>
+  );
+};
+```
diff --git a/src/__stories__/useSet.story.tsx b/src/__stories__/useSet.story.tsx
new file mode 100644
index 00000000..92d92d7b
--- /dev/null
+++ b/src/__stories__/useSet.story.tsx
@@ -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 />);
diff --git a/src/index.ts b/src/index.ts
index 185688e7..1b4dd894 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';
diff --git a/src/useSet.ts b/src/useSet.ts
new file mode 100644
index 00000000..eb79a32b
--- /dev/null
+++ b/src/useSet.ts
@@ -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;


Test patch:
diff --git a/tests/useSet.test.ts b/tests/useSet.test.ts
new file mode 100644
index 00000000..d4744680
--- /dev/null
+++ b/tests/useSet.test.ts
@@ -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);
+});