-
Notifications
You must be signed in to change notification settings - Fork 95
/
index.ts
172 lines (140 loc) · 5.07 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import {type SetStateAction, useMemo, useRef} from 'react';
import {useRerender} from '../useRerender/index.js';
import {useSyncedRef} from '../useSyncedRef/index.js';
import {type InitialState, resolveHookState} from '../util/resolve-hook-state.js';
export type ListActions<T> = {
/**
* Replaces the current list.
*/
set: (newList: SetStateAction<T[]>) => void;
/**
* Adds an item or items to the end of the list.
*/
push: (...items: T[]) => void;
/**
* Replaces the item at the given index of the list. If the given index is out of bounds, empty
* elements are appended to the list until the given item can be set to the given index.
*/
updateAt: (index: number, newItem: T) => void;
/**
* Inserts an item at the given index of the list. All items following the given index are shifted
* one position. If the given index is out of bounds, empty elements are appended to the list until
* the given item can be set to the given index.
*/
insertAt: (index: number, item: T) => void;
/**
* Replaces all items of the list that match the given predicate with the given item.
*/
update: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void;
/**
* Replaces the first item of the list that matches the given predicate with the given item.
*/
updateFirst: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void;
/**
* Replaces the first item of the list that matches the given predicate with the given item. If
* none of the items match the predicate, the given item is pushed to the list.
*/
upsert: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void;
/**
* Sorts the list with the given sorting function. If no sorting function is given, the default
* Array.prototype.sort() sorting is used.
*/
sort: (compareFn?: (a: T, b: T) => number) => void;
/**
* Filters the list with the given filter function.
*/
// We're allowing the type of thisArg to be any, because we are following the Array.prototype.filter API.
filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void;
/**
* Removes the item at the given index of the list. All items following the given index will be
* shifted. If the given index is out of the bounds of the list, the list will not be modified,
* but a rerender will occur.
*/
removeAt: (index: number) => void;
/**
* Deletes all items of the list.
*/
clear: () => void;
/**
* Replaces the current list with the initial list given to this hook.
*/
reset: () => void;
};
export function useList<T>(initialList: InitialState<T[]>): [T[], ListActions<T>] {
const initial = useSyncedRef(initialList);
const list = useRef(resolveHookState(initial.current));
const rerender = useRerender();
const actions = useMemo(
() => ({
set(newList: SetStateAction<T[]>) {
list.current = resolveHookState(newList, list.current);
rerender();
},
push(...items: T[]) {
actions.set((currentList: T[]) => [...currentList, ...items]);
},
updateAt(index: number, newItem: T) {
actions.set((currentList: T[]) => {
const listCopy = [...currentList];
listCopy[index] = newItem;
return listCopy;
});
},
insertAt(index: number, newItem: T) {
actions.set((currentList: T[]) => {
const listCopy = [...currentList];
if (index >= listCopy.length) {
listCopy[index] = newItem;
} else {
listCopy.splice(index, 0, newItem);
}
return listCopy;
});
},
update(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) {
actions.set((currentList: T[]) =>
currentList.map((item: T) => (predicate(item, newItem) ? newItem : item)));
},
updateFirst(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) {
const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem));
const NO_MATCH = -1;
if (indexOfMatch > NO_MATCH) {
actions.updateAt(indexOfMatch, newItem);
}
},
upsert(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) {
const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem));
const NO_MATCH = -1;
if (indexOfMatch > NO_MATCH) {
actions.updateAt(indexOfMatch, newItem);
} else {
actions.push(newItem);
}
},
sort(compareFn?: (a: T, b: T) => number) {
actions.set((currentList: T[]) => [...currentList].sort(compareFn));
},
filter(callbackFn: (value: T, index: number, array: T[]) => boolean, thisArg?: never) {
// eslint-disable-next-line unicorn/no-array-callback-reference,unicorn/no-array-method-this-argument
actions.set((currentList: T[]) => [...currentList].filter(callbackFn, thisArg));
},
removeAt(index: number) {
actions.set((currentList: T[]) => {
const listCopy = [...currentList];
if (index < listCopy.length) {
listCopy.splice(index, 1);
}
return listCopy;
});
},
clear() {
actions.set([]);
},
reset() {
actions.set([...resolveHookState(initial.current)]);
},
}),
[initial, rerender],
);
return [list.current, actions];
}