mirror of
https://github.com/streamich/react-use.git
synced 2025-12-08 18:02:14 +00:00
feat(useList): reimplemented useList hook;
feat(useList): new action upsert; feat(useList): new action update; feat(useList): new action updateFirst; feat(useList): new action insertAt; feat(useList): action remove renamed to removeAt (ref remained); feat(useUpsert): useUpsert hook deprecated cause of duplicate functionality and bad naming;
This commit is contained in:
parent
f094a3ae83
commit
1840b577e2
@ -134,7 +134,7 @@
|
||||
- [`useStateList`](./docs/useStateList.md) — circularly iterates over an array. [![][img-demo]](https://codesandbox.io/s/bold-dewdney-pjzkd)
|
||||
- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean. [![][img-demo]](https://codesandbox.io/s/focused-sammet-brw2d)
|
||||
- [`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)
|
||||
- [`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)
|
||||
- [`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)
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
# `useList`
|
||||
|
||||
React state hook that tracks a value of an array.
|
||||
Tracks an array and provides methods to modify it.
|
||||
To cause component re-render you have to use these methods instead of direct interaction with array - it won't cause re-render.
|
||||
|
||||
We can ensure that actions object and actions itself will not mutate or change between renders, so there is no need to add it to useEffect dependencies and safe to pass them down to children.
|
||||
|
||||
**Note:** `remove` action is deprecated and actually is a copy of `removeAt` action. Within closest updates it will gain different functionality.
|
||||
|
||||
## Usage
|
||||
|
||||
@ -8,7 +13,7 @@ React state hook that tracks a value of an array.
|
||||
import {useList} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [list, { clear, filter, push, remove, set, sort, updateAt, reset }] = useList();
|
||||
const [list, { set, push, updateAt, insertAt, update, updateFirst, upsert, sort, filter, removeAt, clear, reset }] = useList([1, 2, 3, 4, 5]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -27,6 +32,42 @@ const Demo = () => {
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
```ts
|
||||
import {useList} from "react-use";
|
||||
|
||||
const [list, {
|
||||
set,
|
||||
push,
|
||||
updateAt,
|
||||
insertAt,
|
||||
update,
|
||||
updateFirst,
|
||||
upsert,
|
||||
sort,
|
||||
filter,
|
||||
removeAt,
|
||||
remove,
|
||||
clear,
|
||||
reset
|
||||
}] = useList(array: any[] | ()=> any[]);
|
||||
```
|
||||
|
||||
- **`list`**_`: T{}`_ — current list;
|
||||
- **`set`**_`: (list: T[]) => void;`_ — Set new list instead old one;
|
||||
- **`push`**_`: (...items: T[]) => void;`_ — Add item(s) at the end of list;
|
||||
- **`updateAt`**_`: (index: number, item: T) => void;`_ — Replace item at given position. If item at given position not exists it will be set;
|
||||
- **`insertAt`**_`: (index: number, item: T) => void;`_ — Insert item at given position, all items to the right will be shifted;
|
||||
- **`update`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Replace all items that matches predicate with given one;
|
||||
- **`updateFirst`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Replace first item matching predicate with given one;
|
||||
- **`upsert`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Like `updateFirst` bit in case of predicate miss - pushes item to the list;
|
||||
- **`sort`**_`: (compareFn?: (a: T, b: T) => number) => void;`_ — Sort list with given sorting function;
|
||||
- **`filter`**_`: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void;`_ — Same as native Array's method;
|
||||
- **`removeAt`**_`: (index: number) => void;`_ — Removes item at given position. All items to the right from removed will be shifted;
|
||||
- **`remove`**_`: (index: number) => void;`_ — _**DEPRECATED:**_ Use removeAt method instead;
|
||||
- **`clear`**_`: () => void;`_ — Make the list empty;
|
||||
- **`reset`**_`: () => void;`_ — Reset list to initial value;
|
||||
|
||||
## Related hooks
|
||||
|
||||
- [useUpsert](./useUpsert.md)
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
# `useUpsert`
|
||||
|
||||
> DEPRECATED!
|
||||
> Use `useList` hook's upsert action instead
|
||||
|
||||
Superset of [`useList`](./useList.md). Provides an additional method to upsert (update or insert) an element into the list.
|
||||
|
||||
## Usage
|
||||
|
||||
@ -4,24 +4,72 @@ import { useList } from '..';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const [list, { clear, filter, push, remove, set, sort, updateAt, reset }] = useList([1, 2, 3, 4, 5]);
|
||||
const [list, { set, push, updateAt, insertAt, update, updateFirst, sort, filter, removeAt, clear, reset }] = useList([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => set([1, 2, 3])}>Set to [1, 2, 3]</button>
|
||||
<br />
|
||||
<button onClick={() => push(Date.now())}>Push timestamp</button>
|
||||
<br />
|
||||
<button onClick={() => insertAt(1, Date.now())}>Insert new value at index 1</button>
|
||||
<br />
|
||||
<button onClick={() => updateAt(1, Date.now())}>Update value at index 1</button>
|
||||
<button onClick={() => remove(1)}>Remove element at index 1</button>
|
||||
<br />
|
||||
<button onClick={() => removeAt(1)}>Remove element at index 1</button>
|
||||
<br />
|
||||
<button onClick={() => filter(item => item % 2 === 0)}>Filter even values</button>
|
||||
<br />
|
||||
<button onClick={() => update(item => item % 2 === 0, Date.now())}>Update all even values with timestamp</button>
|
||||
<br />
|
||||
<button onClick={() => updateFirst(item => item % 2 === 0, Date.now())}>
|
||||
Update first even value with timestamp
|
||||
</button>
|
||||
<br />
|
||||
<button onClick={() => sort((a, b) => a - b)}>Sort ascending</button>
|
||||
<br />
|
||||
<button onClick={() => sort((a, b) => b - a)}>Sort descending</button>
|
||||
<br />
|
||||
<button onClick={clear}>Clear</button>
|
||||
<br />
|
||||
<button onClick={reset}>Reset</button>
|
||||
<br />
|
||||
<pre>{JSON.stringify(list, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UpsertDemoType {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const upsertPredicate = (a: UpsertDemoType, b: UpsertDemoType) => a.id === b.id;
|
||||
const upsertInitialItems: UpsertDemoType[] = [{ id: '1', text: 'Sample' }, { id: '2', text: 'Example' }];
|
||||
const UpsertDemo = () => {
|
||||
const [list, { upsert, reset, removeAt }] = useList(upsertInitialItems);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
|
||||
{list.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<input value={item.text} onChange={e => upsert(upsertPredicate, { ...item, text: e.target.value })} />
|
||||
<button onClick={() => removeAt(index)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => upsert(upsertPredicate, { id: (list.length + 1).toString(), text: '' })}>Add item</button>
|
||||
<button onClick={() => reset()}>Reset</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('State|useList', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../../docs/useList.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
.add('Demo', () => <Demo />)
|
||||
.add('Upsert Demo', () => <UpsertDemo />);
|
||||
|
||||
@ -1,206 +1,373 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import useList from '../useList';
|
||||
import { useRef } from 'react';
|
||||
import useList, { ListActions } from '../useList';
|
||||
|
||||
const setUp = (initialList?: any[]) => renderHook(() => useList(initialList));
|
||||
|
||||
it('should init list and utils', () => {
|
||||
const { result } = setUp([1, 2, 3]);
|
||||
const [list, utils] = result.current;
|
||||
|
||||
expect(list).toEqual([1, 2, 3]);
|
||||
expect(utils).toStrictEqual({
|
||||
set: expect.any(Function),
|
||||
clear: expect.any(Function),
|
||||
updateAt: expect.any(Function),
|
||||
remove: expect.any(Function),
|
||||
push: expect.any(Function),
|
||||
filter: expect.any(Function),
|
||||
sort: expect.any(Function),
|
||||
reset: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should init empty list if not initial list provided', () => {
|
||||
const { result } = setUp();
|
||||
|
||||
expect(result.current[0]).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set new list', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.set([4, 5, 6]);
|
||||
describe('useList', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useList).toBeDefined();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([4, 5, 6]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
function getHook<T>(initialArray?: T[]) {
|
||||
return renderHook(
|
||||
(props): [number, [T[], ListActions<T>]] => {
|
||||
const counter = useRef(0);
|
||||
|
||||
it('should clear current list', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
return [++counter.current, useList(props)];
|
||||
},
|
||||
{ initialProps: initialArray }
|
||||
);
|
||||
}
|
||||
|
||||
act(() => {
|
||||
utils.clear();
|
||||
});
|
||||
it('should init with 1st parameter and actions', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [list, actions]] = hook.result.current;
|
||||
|
||||
expect(result.current[0]).toEqual([]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should update element at specific position', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.updateAt(1, 'foo');
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 'foo', 3]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should remove element at specific position', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.remove(1);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 3]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should push new element at the end of the list', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.push(0);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 2, 3, 0]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should push duplicated element at the end of the list', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.push(2);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 2, 3, 2]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should push multiple elements at the end of the list', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.push(4, 5, 6);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should filter current list by provided function', () => {
|
||||
const initList = [1, -1, 2, -2, 3, -3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.filter(n => n < 0);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([-1, -2, -3]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should sort current list by default order', () => {
|
||||
const initList = ['March', 'Jan', 'Feb', 'Dec'];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.sort();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual(['Dec', 'Feb', 'Jan', 'March']);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
|
||||
it('should sort current list by provided function', () => {
|
||||
const initList = ['March', 'Jan', 'Feb', 'Dec'];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
|
||||
act(() => {
|
||||
utils.sort((a, b) => {
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
expect(list).toEqual([1, 2, 3]);
|
||||
expect(actions).toStrictEqual({
|
||||
set: expect.any(Function),
|
||||
push: expect.any(Function),
|
||||
updateAt: expect.any(Function),
|
||||
insertAt: expect.any(Function),
|
||||
update: expect.any(Function),
|
||||
updateFirst: expect.any(Function),
|
||||
upsert: expect.any(Function),
|
||||
sort: expect.any(Function),
|
||||
filter: expect.any(Function),
|
||||
removeAt: expect.any(Function),
|
||||
remove: expect.any(Function),
|
||||
clear: expect.any(Function),
|
||||
reset: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual(['March', 'Jan', 'Feb', 'Dec']);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
it('should return the same actions object each render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, actions]] = hook.result.current;
|
||||
|
||||
it('should reset the list to initial list provided', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
act(() => {
|
||||
actions.set([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
utils.push(4);
|
||||
expect(actions).toBe(hook.result.current[1][1]);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 2, 3, 4]);
|
||||
|
||||
act(() => {
|
||||
utils.reset();
|
||||
it('should default with empty array', () => {
|
||||
expect(getHook().result.current[1][0]).toEqual([]);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual([1, 2, 3]);
|
||||
expect(result.current[0]).not.toBe(initList); // checking immutability
|
||||
});
|
||||
describe('set()', () => {
|
||||
it('should reset list with given array and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { set }]] = hook.result.current;
|
||||
|
||||
it('should memoized its utils methods', () => {
|
||||
const initList = [1, 2, 3];
|
||||
const { result } = setUp(initList);
|
||||
const [, utils] = result.current;
|
||||
const { set, clear, updateAt, remove, push, filter, sort, reset } = utils;
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
set([1, 2, 3, 4]);
|
||||
});
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, 4]);
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
|
||||
act(() => {
|
||||
push(4);
|
||||
act(() => {
|
||||
set([1, 2, 3, 4, 5]);
|
||||
});
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, 4, 5]);
|
||||
expect(hook.result.current[0]).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[1]).toBe(utils);
|
||||
expect(result.current[1].set).toBe(set);
|
||||
expect(result.current[1].clear).toBe(clear);
|
||||
expect(result.current[1].updateAt).toBe(updateAt);
|
||||
expect(result.current[1].remove).toBe(remove);
|
||||
expect(result.current[1].push).toBe(push);
|
||||
expect(result.current[1].filter).toBe(filter);
|
||||
expect(result.current[1].sort).toBe(sort);
|
||||
expect(result.current[1].reset).toBe(reset);
|
||||
describe('push()', () => {
|
||||
it('should add arbitrary amount of items to the end and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { push }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
push(1, 2, 3, 4);
|
||||
});
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, 1, 2, 3, 4]);
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
});
|
||||
|
||||
it('should not do anything if called with no parameters', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [list, { push }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
push();
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
expect(list).toBe(hook.result.current[1][0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAt()', () => {
|
||||
it('should replace item at given index with given value and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { updateAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
updateAt(1, 5);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 5, 3]);
|
||||
});
|
||||
|
||||
it('should work fine if target index is out of array length', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { updateAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
updateAt(5, 6);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, undefined, undefined, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertAt()', () => {
|
||||
it('should insert item at given index shifting all the right elements and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { insertAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
insertAt(1, 5);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 5, 2, 3]);
|
||||
});
|
||||
|
||||
it('should work if index is out of array length', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { insertAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
insertAt(5, 6);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, undefined, undefined, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
it('should replace all items that matches the predicate and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { update }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
update((a, _) => a % 2 === 1, 0);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([0, 2, 0]);
|
||||
});
|
||||
|
||||
it('should pass two parameters to the predicate, iterated element and new one', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { update }]] = hook.result.current;
|
||||
const spy = jest.fn();
|
||||
|
||||
act(() => {
|
||||
update(spy, 0);
|
||||
});
|
||||
|
||||
expect(spy.mock.calls[0][0]).toBe(1);
|
||||
expect(spy.mock.calls[0][1]).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFirst()', () => {
|
||||
it('should replace first items that matches the predicate and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { updateFirst }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
updateFirst((a, _) => a % 2 === 1, 0);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([0, 2, 3]);
|
||||
});
|
||||
|
||||
it('should pass two parameters to the predicate, iterated element and new one', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { updateFirst }]] = hook.result.current;
|
||||
const spy = jest.fn();
|
||||
|
||||
act(() => {
|
||||
updateFirst(spy, 0);
|
||||
});
|
||||
|
||||
expect(spy.mock.calls[0].length).toBe(2);
|
||||
expect(spy.mock.calls[0][0]).toBe(1);
|
||||
expect(spy.mock.calls[0][1]).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsert()', () => {
|
||||
it('should replace first item that matches the predicate and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { upsert }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
upsert((a, _) => a === 1, 0);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([0, 2, 3]);
|
||||
});
|
||||
|
||||
it('otherwise should push it to the list and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { upsert }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
upsert((a, _) => a === 5, 0);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, 0]);
|
||||
});
|
||||
|
||||
it('should pass two parameters to the predicate, iterated element and new one', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { upsert }]] = hook.result.current;
|
||||
const spy = jest.fn();
|
||||
|
||||
act(() => {
|
||||
upsert(spy, 0);
|
||||
});
|
||||
|
||||
expect(spy.mock.calls[0].length).toBe(2);
|
||||
expect(spy.mock.calls[0][0]).toBe(1);
|
||||
expect(spy.mock.calls[0][1]).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort()', () => {
|
||||
it('should sort the list with given comparator and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { sort }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
sort((a, b) => (a === b ? 0 : a < b ? 1 : -1));
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([3, 2, 1]);
|
||||
});
|
||||
|
||||
it('should use default array`s sorting function of called without parameters', () => {
|
||||
const hook = getHook([2, 3, 1]);
|
||||
const [, [, { sort }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
sort();
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter()', () => {
|
||||
it('should filter the list with given predicate and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { filter }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
filter(val => val % 2 === 1);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('should pass three parameters to the predicate, iterated element, it`s index and filtered array', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [list, { filter }]] = hook.result.current;
|
||||
const spy = jest.fn((_, _2, _3) => false);
|
||||
|
||||
act(() => {
|
||||
filter(spy);
|
||||
});
|
||||
|
||||
expect(spy.mock.calls[0].length).toBe(3);
|
||||
expect(spy.mock.calls[0][0]).toBe(1);
|
||||
expect(spy.mock.calls[0][1]).toBe(0);
|
||||
expect(spy.mock.calls[0][2]).toEqual(list);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAt()', () => {
|
||||
it('should remove item at given index and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { removeAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
removeAt(1);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('should do nothing if index is out of array length, although it should cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { removeAt }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
removeAt(5);
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove()', () => {
|
||||
it('should be a ref to removeAt', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { remove, removeAt }]] = hook.result.current;
|
||||
|
||||
expect(remove).toBe(removeAt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear()', () => {
|
||||
it('should clear the list and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { clear }]] = hook.result.current;
|
||||
|
||||
expect(hook.result.current[0]).toBe(1);
|
||||
act(() => {
|
||||
clear();
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
expect(hook.result.current[1][0]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset()', () => {
|
||||
it('should reset list to initial values and cause re-render', () => {
|
||||
const hook = getHook([1, 2, 3]);
|
||||
const [, [, { set, reset }]] = hook.result.current;
|
||||
|
||||
act(() => {
|
||||
set([1, 2, 3, 4, 6, 7, 8]);
|
||||
});
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3, 4, 6, 7, 8]);
|
||||
|
||||
expect(hook.result.current[0]).toBe(2);
|
||||
act(() => {
|
||||
reset();
|
||||
});
|
||||
expect(hook.result.current[0]).toBe(3);
|
||||
expect(hook.result.current[1][0]).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
171
src/useList.ts
171
src/useList.ts
@ -1,35 +1,154 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useUpdate from './useUpdate';
|
||||
import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';
|
||||
|
||||
export interface Actions<T> {
|
||||
set: (list: T[]) => void;
|
||||
clear: () => void;
|
||||
updateAt: (index: number, item: T) => void;
|
||||
remove: (index: number) => void;
|
||||
export interface ListActions<T> {
|
||||
/**
|
||||
* @description Set new list instead old one
|
||||
*/
|
||||
set: (newList: ResolvableHookState<T[]>) => void;
|
||||
/**
|
||||
* @description Add item(s) at the end of list
|
||||
*/
|
||||
push: (...items: T[]) => void;
|
||||
filter: (fn: (value: T) => boolean) => void;
|
||||
sort: (fn?: (a: T, b: T) => number) => void;
|
||||
|
||||
/**
|
||||
* @description Replace item at given position. If item at given position not exists it will be set.
|
||||
*/
|
||||
updateAt: (index: number, item: T) => void;
|
||||
/**
|
||||
* @description Insert item at given position, all items to the right will be shifted.
|
||||
*/
|
||||
insertAt: (index: number, item: T) => void;
|
||||
|
||||
/**
|
||||
* @description Replace all items that matches predicate with given one.
|
||||
*/
|
||||
update: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
|
||||
/**
|
||||
* @description Replace first item matching predicate with given one.
|
||||
*/
|
||||
updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
|
||||
/**
|
||||
* @description Like `updateFirst` bit in case of predicate miss - pushes item to the list
|
||||
*/
|
||||
upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
|
||||
|
||||
/**
|
||||
* @description Sort list with given sorting function
|
||||
*/
|
||||
sort: (compareFn?: (a: T, b: T) => number) => void;
|
||||
/**
|
||||
* @description Same as native Array's method
|
||||
*/
|
||||
filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void;
|
||||
|
||||
/**
|
||||
* @description Removes item at given position. All items to the right from removed will be shifted.
|
||||
*/
|
||||
removeAt: (index: number) => void;
|
||||
/**
|
||||
* @deprecated Use removeAt method instead
|
||||
*/
|
||||
remove: (index: number) => void;
|
||||
|
||||
/**
|
||||
* @description Make the list empty
|
||||
*/
|
||||
clear: () => void;
|
||||
/**
|
||||
* @description Reset list to initial value
|
||||
*/
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const useList = <T>(initialList: T[] = []): [T[], Actions<T>] => {
|
||||
const [list, set] = useState<T[]>(initialList);
|
||||
function useList<T>(initialList: InitialHookState<T[]> = []): [T[], ListActions<T>] {
|
||||
const list = useRef(resolveHookState(initialList));
|
||||
const update = useUpdate();
|
||||
|
||||
const utils = useMemo<Actions<T>>(
|
||||
() => ({
|
||||
set,
|
||||
clear: () => set([]),
|
||||
updateAt: (index, entry) =>
|
||||
set(currentList => [...currentList.slice(0, index), entry, ...currentList.slice(index + 1)]),
|
||||
remove: index => set(currentList => [...currentList.slice(0, index), ...currentList.slice(index + 1)]),
|
||||
push: (...entry) => set(currentList => [...currentList, ...entry]),
|
||||
filter: fn => set(currentList => currentList.filter(fn)),
|
||||
sort: (fn?) => set(currentList => [...currentList].sort(fn)),
|
||||
reset: () => set([...initialList]),
|
||||
}),
|
||||
[set]
|
||||
);
|
||||
const actions = useMemo<ListActions<T>>(() => {
|
||||
const a = {
|
||||
set: (newList: ResolvableHookState<T[]>) => {
|
||||
list.current = resolveHookState(newList, list.current);
|
||||
update();
|
||||
},
|
||||
|
||||
return [list, utils];
|
||||
};
|
||||
push: (...items: T[]) => {
|
||||
items.length && actions.set((curr: T[]) => curr.concat(items));
|
||||
},
|
||||
|
||||
updateAt: (index: number, item: T) => {
|
||||
actions.set((curr: T[]) => {
|
||||
const arr = curr.slice();
|
||||
|
||||
arr[index] = item;
|
||||
|
||||
return arr;
|
||||
});
|
||||
},
|
||||
|
||||
insertAt: (index: number, item: T) => {
|
||||
actions.set((curr: T[]) => {
|
||||
const arr = curr.slice();
|
||||
|
||||
index > arr.length ? (arr[index] = item) : arr.splice(index, 0, item);
|
||||
|
||||
return arr;
|
||||
});
|
||||
},
|
||||
|
||||
update: (predicate: (a: T, b: T) => boolean, newItem: T) => {
|
||||
actions.set((curr: T[]) => curr.map(item => (predicate(item, newItem) ? newItem : item)));
|
||||
},
|
||||
|
||||
updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => {
|
||||
const index = list.current.findIndex(item => predicate(item, newItem));
|
||||
|
||||
index >= 0 && actions.updateAt(index, newItem);
|
||||
},
|
||||
|
||||
upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => {
|
||||
const index = list.current.findIndex(item => predicate(item, newItem));
|
||||
|
||||
index >= 0 ? actions.updateAt(index, newItem) : actions.push(newItem);
|
||||
},
|
||||
|
||||
sort: (compareFn?: (a: T, b: T) => number) => {
|
||||
actions.set((curr: T[]) => curr.slice().sort(compareFn));
|
||||
},
|
||||
|
||||
filter: <S extends T>(callbackFn: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => {
|
||||
actions.set((curr: T[]) => curr.slice().filter(callbackFn, thisArg));
|
||||
},
|
||||
|
||||
removeAt: (index: number) => {
|
||||
actions.set((curr: T[]) => {
|
||||
const arr = curr.slice();
|
||||
|
||||
arr.splice(index, 1);
|
||||
|
||||
return arr;
|
||||
});
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
actions.set([]);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
actions.set(resolveHookState(initialList).slice());
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use removeAt method instead
|
||||
*/
|
||||
(a as ListActions<T>).remove = a.removeAt;
|
||||
|
||||
return a as ListActions<T>;
|
||||
}, []);
|
||||
|
||||
return [list.current, actions];
|
||||
}
|
||||
|
||||
export default useList;
|
||||
|
||||
@ -1,39 +1,26 @@
|
||||
import useList, { Actions as ListActions } from './useList';
|
||||
import useList, { ListActions } from './useList';
|
||||
import { InitialHookState } from './util/resolveHookState';
|
||||
|
||||
export interface Actions<T> extends ListActions<T> {
|
||||
upsert: (item: T) => void;
|
||||
export interface UpsertListActions<T> extends Omit<ListActions<T>, 'upsert'> {
|
||||
upsert: (newItem: T) => void;
|
||||
}
|
||||
|
||||
const useUpsert = <T>(
|
||||
comparisonFunction: (upsertedItem: T, existingItem: T) => boolean,
|
||||
initialList: T[] = []
|
||||
): [T[], Actions<T>] => {
|
||||
const [items, actions] = useList(initialList);
|
||||
|
||||
const upsert = (upsertedItem: T) => {
|
||||
let itemWasFound = false;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const existingItem = items[i];
|
||||
|
||||
const shouldUpdate = comparisonFunction(existingItem, upsertedItem);
|
||||
if (shouldUpdate) {
|
||||
actions.updateAt(i, upsertedItem);
|
||||
itemWasFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!itemWasFound) {
|
||||
actions.push(upsertedItem);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `useList` hook's upsert action instead
|
||||
*/
|
||||
export default function useUpsert<T>(
|
||||
predicate: (a: T, b: T) => boolean,
|
||||
initialList: InitialHookState<T[]> = []
|
||||
): [T[], UpsertListActions<T>] {
|
||||
const [list, listActions] = useList(initialList);
|
||||
|
||||
return [
|
||||
items,
|
||||
list,
|
||||
{
|
||||
...actions,
|
||||
upsert,
|
||||
},
|
||||
...listActions,
|
||||
upsert: (newItem: T) => {
|
||||
listActions.upsert(predicate, newItem);
|
||||
},
|
||||
} as UpsertListActions<T>,
|
||||
];
|
||||
};
|
||||
|
||||
export default useUpsert;
|
||||
}
|
||||
|
||||
@ -5,9 +5,10 @@ export type InitialHookState<S> = S | InitialStateSetter<S>;
|
||||
export type HookState<S> = S | StateSetter<S>;
|
||||
export type ResolvableHookState<S> = S | StateSetter<S> | InitialStateSetter<S>;
|
||||
|
||||
export function resolveHookState<S, C extends S>(newState: StateSetter<S>, currentState: C): S;
|
||||
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S;
|
||||
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S {
|
||||
export function resolveHookState<S>(newState: InitialStateSetter<S>): S;
|
||||
export function resolveHookState<S>(newState: StateSetter<S>, currentState: S): S;
|
||||
export function resolveHookState<S>(newState: ResolvableHookState<S>, currentState?: S): S;
|
||||
export function resolveHookState<S>(newState: ResolvableHookState<S>, currentState?: S): S {
|
||||
if (typeof newState === 'function') {
|
||||
return (newState as Function)(currentState);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user