Skip to content

Commit f10f7cb

Browse files
pblazhRIP21
authored andcommitted
Add useSet
1 parent 79931f5 commit f10f7cb

8 files changed

Lines changed: 205 additions & 17 deletions

File tree

README-ARRAY.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const [[newTodo], actions] = useInput("");
6666
```jsx
6767
<input value={newTodo} onChange={actions.onChange} />
6868
```
69-
Actions:
69+
UseSetActions:
7070

7171
- `clear`
7272
- `onChange` - default native event.target.value handler
@@ -91,7 +91,7 @@ const [[newTodo], actions, { nativeBind, valueBind }] = useBindToInput(useInput(
9191
<Slider {...valueBind} />
9292
```
9393

94-
Actions:
94+
UseSetActions:
9595

9696
- `nativeBind` - binds the `value` and `onChange` props to an input that has `e.target.value`
9797
- `valueBind` - binds the `value` and `onChange` props to an input that's using only `value` in `onChange` (like most external components)
@@ -102,7 +102,7 @@ Actions:
102102
const [todos, actions] = useArray([]);
103103
```
104104

105-
Actions:
105+
UseSetActions:
106106

107107
- `push`
108108
- `unshift`
@@ -139,7 +139,21 @@ Actions:
139139
- `clear`
140140
- `initialize` - applies tuples or map instances
141141
- `setValue`
142-
142+
143+
### useSet
144+
145+
```jsx
146+
const [ value, actions ] = useSet(new Set<number>([1, 2]))
147+
```
148+
149+
`value` - a Set with only non mutating methods of a plain JS Set
150+
151+
Actions:
152+
153+
- `setValue`
154+
- `add`
155+
- `remove`
156+
- `clear`
143157

144158
## useSetState
145159

@@ -221,4 +235,4 @@ const [[value, hasValue], actions, { eventBind, valueBind }] = useBindToInput(us
221235
```
222236

223237
Note that first element in destructured array has tuple of `[value, hasValue]` since it's for values
224-
and second argument is for `actions` e.g. only for functions.
238+
and second argument is for `actions` e.g. only for functions.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ Actions:
216216
- `initialize` - applies tuples or map instances
217217
- `setValue`
218218

219+
### useSet
220+
221+
```jsx
222+
const set = useSet(new Set<number>([1, 2]));
223+
```
224+
219225
### useSetState
220226

221227
```jsx

src/array/index.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useInput } from './useInput';
77
import { useBindToInput } from './useBindToInput';
88
import { useSetState } from './useSetState';
99
import { useMap } from './useMap';
10+
import { useSet } from './useSet';
1011

1112
afterEach(cleanup);
1213
describe('useNumber array', () => {
@@ -407,6 +408,76 @@ describe('useBoolean array', () => {
407408
});
408409
});
409410

411+
describe('useSet array', () => {
412+
const initial = new Set([1, 2, 3]);
413+
414+
it('should update old value', () => {
415+
// given
416+
const { result } = renderHook(() => useSet<number>(initial));
417+
const [value, { setValue }] = result.current;
418+
419+
expect(value).toEqual(initial);
420+
// when
421+
act(() => setValue(new Set([2])));
422+
// then
423+
expect(result.current[0]).toEqual(new Set([2]));
424+
});
425+
426+
it('should add new value', () => {
427+
// given
428+
const { result } = renderHook(() => useSet<number>(initial));
429+
const [, { add }] = result.current;
430+
// when
431+
act(() => add(4));
432+
// then
433+
expect(result.current[0]).toEqual(new Set([1, 2, 3, 4]));
434+
});
435+
436+
it('should remove a value', () => {
437+
// given
438+
const { result } = renderHook(() => useSet<number>(initial));
439+
const [, { remove }] = result.current;
440+
// when
441+
act(() => remove(2));
442+
// then
443+
expect(result.current[0]).toEqual(new Set([1, 3]));
444+
});
445+
446+
it('should clear', () => {
447+
// given
448+
const { result } = renderHook(() => useSet<number>(initial));
449+
const [, { clear }] = result.current;
450+
// when
451+
act(() => clear());
452+
// then
453+
expect(result.current[0]).toEqual(new Set());
454+
});
455+
456+
describe('hooks optimizations', () => {
457+
it('should change value reference equality after change', () => {
458+
// given
459+
const { result } = renderHook(() => useSet<number>(initial));
460+
const [originalValueReference, actions] = result.current;
461+
expect(result.current[0]).toBe(originalValueReference);
462+
// when
463+
act(() => actions.setValue(new Set([1])));
464+
// then
465+
expect(originalValueReference).not.toBe(result.current[0]);
466+
});
467+
468+
it('should keep actions reference equality after value change', () => {
469+
// given
470+
const { result } = renderHook(() => useSet<number>(initial));
471+
const [, originalActionsReference] = result.current;
472+
expect(result.current[1]).toBe(originalActionsReference);
473+
// when
474+
act(() => originalActionsReference.setValue(new Set([1])));
475+
// then
476+
expect(originalActionsReference).toBe(result.current[1]);
477+
});
478+
});
479+
});
480+
410481
describe('useMap array', () => {
411482
describe('set', () => {
412483
it('should update old value', () => {

src/array/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
export { useBindToInput, UseBindToInput, BindToInput } from './useBindToInput';
21
export { useArray, UseArray, UseArrayActions } from './useArray';
2+
export { useBindToInput, UseBindToInput, BindToInput } from './useBindToInput';
33
export { useBoolean, UseBoolean, UseBooleanActions } from './useBoolean';
44
export { useInput, UseInput, UseInputActions } from './useInput';
55
export { useMap, UseMap, MapOrEntries, UseMapActions, UseMapFunctions } from './useMap';
66
export { useNumber, UseNumber, UseNumberActions } from './useNumber';
7+
export { useSet, UseSet, UseSetActions } from './useSet';
78
export { useSetState, UseSetState, UseSetStateAction } from './useSetState';

src/array/useSet.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
2+
3+
export type UseSetActions<T> = {
4+
setValue: Dispatch<SetStateAction<Set<T>>>;
5+
add: (a: T) => void;
6+
remove: (a: T) => void;
7+
clear: Set<T>['clear'];
8+
};
9+
10+
export type UseSet<T> = [Set<T>, UseSetActions<T>];
11+
12+
const clone = <T>(value: Set<T>) => new Set<T>(value);
13+
14+
export function useSet<T>(initialState: Set<T> = new Set()): UseSet<T> {
15+
const [value, setValue] = useState(initialState);
16+
const add = useCallback((item: T) => {
17+
setValue((prevValue) => {
18+
const copy = clone(prevValue);
19+
copy.add(item);
20+
return copy;
21+
});
22+
}, []);
23+
24+
const remove = useCallback((item: T) => {
25+
setValue((prevValue) => {
26+
const copy = clone(prevValue);
27+
copy.delete(item);
28+
return copy;
29+
});
30+
}, []);
31+
32+
const clear = useCallback(() => {
33+
setValue((prevValue) => {
34+
const copy = clone(prevValue);
35+
copy.clear();
36+
return copy;
37+
});
38+
}, []);
39+
40+
const actions: UseSetActions<T> = useMemo(
41+
() => ({
42+
setValue,
43+
add,
44+
remove,
45+
clear,
46+
}),
47+
[add, clear, remove],
48+
);
49+
50+
return useMemo(() => [value, actions], [value, actions]);
51+
}

src/index.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useBoolean } from './useBoolean';
77
import { useInput } from './useInput';
88
import { useSetState } from './useSetState';
99
import { useMap } from './useMap';
10+
import { useSet } from './useSet';
1011

1112
afterEach(cleanup);
1213
describe('useStateful', () => {
@@ -370,6 +371,33 @@ describe('useBoolean', () => {
370371
});
371372
});
372373

374+
describe('useSet', () => {
375+
const initial = new Set([1, 2, 3]);
376+
377+
describe('hooks optimizations', () => {
378+
it('should change value reference equality after change', () => {
379+
// given
380+
const { result } = renderHook(() => useSet<number>());
381+
const value = result.current;
382+
// when
383+
act(() => value.setValue(initial));
384+
// then
385+
expect(value).not.toBe(result.current);
386+
});
387+
388+
it('should keep actions reference equality after value change', () => {
389+
// given
390+
const { result } = renderHook(() => useSet<number>());
391+
const { setValue } = result.current;
392+
expect(result.current.setValue).toBe(setValue);
393+
// when
394+
act(() => setValue(new Set([1, 1])));
395+
// then
396+
expect(setValue).toBe(result.current.setValue);
397+
});
398+
});
399+
});
400+
373401
describe('useMap', () => {
374402
describe('set', () => {
375403
it('should update old value', () => {

src/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1+
export { ClickOutsideOptions, useOnClickOutside } from './useClickOutside';
12
export { useArray, UseArray } from './useArray';
23
export { useBoolean, UseBoolean } from './useBoolean';
4+
export { useDelay } from './useDelay';
5+
export { useDocumentReady } from './useDocumentReady';
6+
export { useFocus } from './useFocus';
7+
export { useGoogleAnalytics, UseGoogleAnalyticsProps } from './useGoogleAnalytics';
8+
export { useImage } from './useImage';
39
export { useInput, UseInput } from './useInput';
410
export { useLogger } from './useLogger';
511
export { useMap, UseMap, MapOrEntries } from './useMap';
612
export { useNumber, UseNumber } from './useNumber';
13+
export { useOnClick } from './useOnClick';
14+
export { usePageLoad } from './usePageLoad';
15+
export { usePersist } from './usePersist';
716
export { usePrevious } from './usePrevious';
17+
export { UseScript, useScript, UseScriptProps } from './useScript';
18+
export { useSet, UseSet } from './useSet';
819
export { useSetState, UseSetState, UseSetStateAction } from './useSetState';
920
export { useStateful, UseStateful } from './useStateful';
10-
export { usePageLoad } from './usePageLoad';
11-
export { UseScript, useScript, UseScriptProps } from './useScript';
12-
export { useDocumentReady } from './useDocumentReady';
13-
export { useGoogleAnalytics, UseGoogleAnalyticsProps } from './useGoogleAnalytics';
14-
export { useWindowSize } from './useWindowSize';
15-
export { useDelay } from './useDelay';
16-
export { usePersist } from './usePersist';
1721
export { useToggleBodyClass } from './useToggleBodyClass';
18-
export { useOnClick } from './useOnClick';
19-
export { ClickOutsideOptions, useOnClickOutside } from './useClickOutside';
20-
export { useFocus } from './useFocus';
21-
export { useImage } from './useImage';
22+
export { useWindowSize } from './useWindowSize';

src/useSet.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useMemo } from 'react';
2+
import { useSet as useSetArray, UseSetActions } from './array/useSet';
3+
import type { UseStateful } from './useStateful';
4+
5+
export interface UseSet<T> extends UseStateful<Set<T>>, UseSetActions<T> {}
6+
7+
export function useSet<T>(initialState: Set<T> = new Set()): UseSet<T> {
8+
const [value, actions] = useSetArray(initialState);
9+
10+
return useMemo(() => {
11+
return {
12+
value,
13+
...actions,
14+
};
15+
}, [actions, value]);
16+
}

0 commit comments

Comments
 (0)