@learncard/helpers 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/state.ts ADDED
@@ -0,0 +1,576 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ import { produce, Draft, castDraft } from 'immer';
3
+ import { DraftFunction, Updater } from 'use-immer';
4
+
5
+ export type SetState<T> = Dispatch<SetStateAction<T>>;
6
+
7
+ // If you're curious about this file, start with curriedStateSlice down at the bottom, then assume
8
+ // you're using immer everywhere and checkout curriedInnerImmerOuterImmer, which is a curry wrapper
9
+ // around innerImmerOuterImmer
10
+
11
+ type ImmerOrReact = 'immer' | 'react';
12
+
13
+ type UpdaterArgs<T> = T | DraftFunction<T>;
14
+
15
+ type CurriedReturn<ValueArg, Value> = undefined extends ValueArg
16
+ ? (innerValue: Value) => void
17
+ : void;
18
+
19
+ type InnerImmerOuterImmer<State> = <
20
+ Field extends keyof Draft<State>,
21
+ Value extends UpdaterArgs<Draft<State>[Field]>,
22
+ ValueArg extends Value | undefined
23
+ >(
24
+ field: Field,
25
+ value?: ValueArg
26
+ ) => CurriedReturn<ValueArg, Value>;
27
+
28
+ type InnerReactOuterImmer<State> = <
29
+ Field extends keyof Draft<State>,
30
+ Value extends SetStateAction<Draft<State>[Field]>,
31
+ ValueArg extends Value | undefined
32
+ >(
33
+ field: Field,
34
+ value?: ValueArg
35
+ ) => CurriedReturn<ValueArg, Value>;
36
+
37
+ type InnerImmerOuterReact<State> = <
38
+ Field extends keyof State,
39
+ Value extends UpdaterArgs<State[Field]>,
40
+ ValueArg extends Value | undefined
41
+ >(
42
+ field: Field,
43
+ value?: ValueArg
44
+ ) => CurriedReturn<ValueArg, Value>;
45
+
46
+ type InnerReactOuterReact<State> = <
47
+ Field extends keyof State,
48
+ Value extends SetStateAction<State[Field]>,
49
+ ValueArg extends Value | undefined
50
+ >(
51
+ field: Field,
52
+ value?: ValueArg
53
+ ) => CurriedReturn<ValueArg, Value>;
54
+
55
+ export function innerImmerOuterImmer<
56
+ State,
57
+ Field extends keyof Draft<State>,
58
+ Value extends UpdaterArgs<Draft<State>[Field]>
59
+ >(setState: Updater<State>, field: Field): (innerValue: NonNullable<Value>) => void;
60
+ export function innerImmerOuterImmer<
61
+ State,
62
+ Field extends keyof Draft<State>,
63
+ Value extends UpdaterArgs<Draft<State>[Field]>,
64
+ ValueArg extends Value | undefined
65
+ >(setState: Updater<State>, field: Field, value: ValueArg): void;
66
+
67
+ /**
68
+ * Creates a useImmer Updater style function for a given field. This is useful for creating mini
69
+ * pieces of state from larger pieces of state, or for creating manual setState functions for
70
+ * components that expect setState as a prop.
71
+ *
72
+ * This function assumes you are using useImmer instead of useState
73
+ *
74
+ * Use like so:
75
+ *
76
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
77
+ *
78
+ * innerImmerOuterImmer(setState, 'num', 3); // Updates state to { num: 3, string: 'str' }
79
+ *
80
+ * innerImmerOuterImmer(
81
+ * setState,
82
+ * 'num',
83
+ * oldNum => {
84
+ * oldNum += 1;
85
+ * },
86
+ * ); // Updates state to { num: 4, string: 'str' }
87
+ *
88
+ * innerImmerOuterImmer(setState, 'num', 'three'); // TS Error due to invalid type
89
+ *
90
+ */
91
+ export function innerImmerOuterImmer<
92
+ State,
93
+ Field extends keyof Draft<State>,
94
+ Value extends UpdaterArgs<Draft<State>[Field]>,
95
+ ValueArg extends Value | undefined
96
+ >(setState: Updater<State>, field: Field, value?: ValueArg): any {
97
+ if (value === undefined) {
98
+ return (innerValue: NonNullable<Value>) => {
99
+ setState((oldState: Draft<State>) => {
100
+ if (typeof innerValue === 'function') innerValue(oldState[field]);
101
+ else oldState[field] = innerValue as Draft<State>[Field];
102
+ });
103
+ };
104
+ }
105
+
106
+ setState((oldState: Draft<State>) => {
107
+ if (typeof value === 'function') value(oldState[field]);
108
+ else oldState[field] = value as Draft<State>[Field];
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Curry function to allow innerImmerOuterImmer to be used without constantly passing in setState
114
+ *
115
+ * Use like so:
116
+ *
117
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
118
+ *
119
+ * const updateField = curriedInnerImmerOuterImmer(setState);
120
+ *
121
+ * updateField('num', 3); // Updates state to { num: 3, string: 'str' }
122
+ *
123
+ * updateField(
124
+ * 'num',
125
+ * oldNum => {
126
+ * oldNum += 1;
127
+ * },
128
+ * ); // Updates state to { num: 4, string: 'str' }
129
+ *
130
+ * updateField(setState, 'num', 'three'); // TS Error due to invalid type
131
+ */
132
+ export const curriedInnerImmerOuterImmer =
133
+ <State>(setState: Updater<State>): InnerImmerOuterImmer<State> =>
134
+ <
135
+ Field extends keyof Draft<State>,
136
+ Value extends UpdaterArgs<Draft<State>[Field]>,
137
+ ValueArg extends Value | undefined
138
+ >(
139
+ field: Field,
140
+ value?: ValueArg
141
+ ) =>
142
+ innerImmerOuterImmer<State, Field, Value, ValueArg>(setState, field, value as any) as any;
143
+
144
+ export function innerReactOuterImmer<
145
+ State,
146
+ Field extends keyof Draft<State>,
147
+ Value extends SetStateAction<Draft<State>[Field]>
148
+ >(setState: Updater<State>, field: Field): (innerValue: NonNullable<Value>) => void;
149
+
150
+ export function innerReactOuterImmer<
151
+ State,
152
+ Field extends keyof Draft<State>,
153
+ Value extends SetStateAction<Draft<State>[Field]>,
154
+ ValueArg extends Value | undefined
155
+ >(setState: Updater<State>, field: Field, value: ValueArg): void;
156
+ /**
157
+ * Creates a React setState style function for a given field. This is useful for creating mini
158
+ * pieces of state from larger pieces of state, or for creating manual setState functions for
159
+ * components that expect setState as a prop.
160
+ *
161
+ * This function assumes you are using useImmer instead of useState
162
+ *
163
+ * Use like so:
164
+ *
165
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
166
+ *
167
+ * innerReactOuterImmer(setState, 'num', 3); // Updates state to { num: 3, string: 'str' }
168
+ *
169
+ * innerReactOuterImmer(
170
+ * setState,
171
+ * 'num',
172
+ * oldNum => oldNum + 1,
173
+ * ); // Updates state to { num: 4, string: 'str' }
174
+ *
175
+ * innerReactOuterImmer(setState, 'num', 'three'); // TS Error due to invalid type
176
+ *
177
+ */
178
+ export function innerReactOuterImmer<
179
+ State,
180
+ Field extends keyof Draft<State>,
181
+ Value extends SetStateAction<Draft<State>[Field]>,
182
+ ValueArg extends Value | undefined
183
+ >(setState: Updater<State>, field: Field, value?: ValueArg) {
184
+ if (value === undefined) {
185
+ return (innerValue: NonNullable<Value>) => {
186
+ setState((oldState: Draft<State>) => {
187
+ if (typeof innerValue === 'function') innerValue(oldState[field]);
188
+ else oldState[field] = innerValue as Draft<State>[Field];
189
+ });
190
+ };
191
+ }
192
+
193
+ setState(oldState => {
194
+ oldState[field] =
195
+ typeof value === 'function'
196
+ ? (value(oldState[field]) as Draft<State>[Field])
197
+ : (value as Draft<State>[Field]);
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Curry function to allow innerReactOuterImmer to be used without constantly passing in setState
203
+ *
204
+ * Use like so:
205
+ *
206
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
207
+ *
208
+ * const updateField = curriedUpdateImmerSetStateField(setState);
209
+ *
210
+ * updateField('num', 3); // Updates state to { num: 3, string: 'str' }
211
+ *
212
+ * updateField('num', oldNum => oldNum + 1); // Updates state to { num: 4, string: 'str' }
213
+ *
214
+ * updateField(setState, 'num', 'three'); // TS Error due to invalid type
215
+ */
216
+ export const curriedInnerReactOuterImmer =
217
+ <State>(setState: Updater<State>): InnerReactOuterImmer<State> =>
218
+ <
219
+ Field extends keyof Draft<State>,
220
+ Value extends SetStateAction<Draft<State>[Field]>,
221
+ ValueArg extends Value | undefined
222
+ >(
223
+ field: Field,
224
+ value?: ValueArg
225
+ ) =>
226
+ innerReactOuterImmer<State, Field, Value, ValueArg>(setState, field, value as any) as any;
227
+
228
+ export function innerImmerOuterReact<
229
+ State,
230
+ Field extends keyof State,
231
+ Value extends UpdaterArgs<State[Field]>
232
+ >(setState: SetState<State>, field: Field): (innerValue: NonNullable<Value>) => void;
233
+ export function innerImmerOuterReact<
234
+ State,
235
+ Field extends keyof State,
236
+ Value extends UpdaterArgs<State[Field]>,
237
+ ValueArg extends Value | undefined
238
+ >(setState: SetState<State>, field: Field, value: ValueArg): void;
239
+ /**
240
+ * Creates a useImmer Updater style function for a given field. This is useful for creating mini
241
+ * pieces of state from larger pieces of state, or for creating manual setState functions for
242
+ * components that expect setState as a prop.
243
+ *
244
+ * If possible, please consider updating your useState calls to useImmer and using the immer
245
+ * versions of these functions
246
+ *
247
+ * Use like so:
248
+ *
249
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
250
+ *
251
+ * innerImmerOuterReact(setState, 'num', 3); // Updates state to { num: 3, string: 'str' }
252
+ *
253
+ * innerImmerOuterReact(
254
+ * setState,
255
+ * 'num',
256
+ * oldNum => {
257
+ * oldNum += 1;
258
+ * },
259
+ * ); // Updates state to { num: 4, string: 'str' }
260
+ *
261
+ * innerImmerOuterReact(setState, 'num', 'three'); // TS Error due to invalid type
262
+ *
263
+ */
264
+ export function innerImmerOuterReact<
265
+ State,
266
+ Field extends keyof State,
267
+ Value extends UpdaterArgs<State[Field]>,
268
+ ValueArg extends Value | undefined
269
+ >(setState: SetState<State>, field: Field, value?: ValueArg) {
270
+ if (value === undefined) {
271
+ return (innerValue: NonNullable<Value>) => {
272
+ if (typeof innerValue === 'function') {
273
+ setState(produce((oldState: any) => innerValue(oldState[field])));
274
+ } else setState((oldState: State) => ({ ...oldState, [field]: innerValue }));
275
+ };
276
+ }
277
+
278
+ if (typeof value === 'function') {
279
+ setState(produce((oldState: any) => value(oldState[field])));
280
+ } else setState((oldState: State) => ({ ...oldState, [field]: value }));
281
+ }
282
+
283
+ /**
284
+ * Curry function to allow innerImmerOuterReact to be used without constantly passing in setState
285
+ *
286
+ * Use like so:
287
+ *
288
+ * [state, setState] = useImmer<{ num: number; string: string; }>({ num: 1, string: 'str' });
289
+ *
290
+ * const updateField = curriedInnerImmerOuterImmer(setState);
291
+ *
292
+ * updateField('num', 3); // Updates state to { num: 3, string: 'str' }
293
+ *
294
+ * updateField(
295
+ * 'num',
296
+ * oldNum => {
297
+ * oldNum += 1;
298
+ * },
299
+ * ); // Updates state to { num: 4, string: 'str' }
300
+ *
301
+ * updateField(setState, 'num', 'three'); // TS Error due to invalid type
302
+ */
303
+ export const curriedInnerImmerOuterReact =
304
+ <State>(setState: SetState<State>): InnerImmerOuterReact<State> =>
305
+ <
306
+ Field extends keyof State,
307
+ Value extends UpdaterArgs<State[Field]>,
308
+ ValueArg extends Value | undefined
309
+ >(
310
+ field: Field,
311
+ value?: ValueArg
312
+ ) =>
313
+ innerImmerOuterReact<State, Field, Value, ValueArg>(setState, field, value as any) as any;
314
+
315
+ export function innerReactOuterReact<
316
+ State,
317
+ Field extends keyof State,
318
+ Value extends SetStateAction<State[Field]>
319
+ >(setState: SetState<State>, field: Field): (innerValue: NonNullable<Value>) => void;
320
+ export function innerReactOuterReact<
321
+ State,
322
+ Field extends keyof State,
323
+ Value extends SetStateAction<State[Field]>,
324
+ ValueArg extends Value | undefined
325
+ >(setState: SetState<State>, field: Field, value: ValueArg): void;
326
+ /**
327
+ * Creates a React setState style function for a given field. This is useful for creating mini
328
+ * pieces of state from larger pieces of state, or for creating manual setState functions for
329
+ * components that expect setState as a prop.
330
+ *
331
+ * If possible, please consider updating your useState calls to useImmer and using the immer
332
+ * versions of these functions
333
+ *
334
+ * Use like so:
335
+ *
336
+ * [state, setState] = useState<{ num: number; string: string; }>({ num: 1, string: 'str' });
337
+ *
338
+ * innerReactOuterReact(setState, 'num', 3); // Updates state to { num: 3, string: 'str' }
339
+ *
340
+ * innerReactOuterReact(
341
+ * setState,
342
+ * 'num',
343
+ * oldNum => oldNum + 1,
344
+ * ); // Updates state to { num: 4, string: 'str' }
345
+ *
346
+ * innerReactOuterReact(setState, 'num', 'three'); // TS Error due to invalid type
347
+ *
348
+ */
349
+ export function innerReactOuterReact<
350
+ State,
351
+ Field extends keyof State,
352
+ Value extends SetStateAction<State[Field]>,
353
+ ValueArg extends Value | undefined
354
+ >(setState: SetState<State>, field: Field, value?: ValueArg) {
355
+ if (value === undefined) {
356
+ return (innerValue: NonNullable<Value>) => {
357
+ setState((oldState: State) => ({
358
+ ...oldState,
359
+ [field]:
360
+ typeof innerValue === 'function' ? innerValue(oldState[field]) : innerValue,
361
+ }));
362
+ };
363
+ }
364
+
365
+ setState((oldState: State) => ({
366
+ ...oldState,
367
+ [field]: typeof value === 'function' ? value(oldState[field]) : value,
368
+ }));
369
+ }
370
+
371
+ /**
372
+ * Curry function to allow updateSetStateField to be used without constantly passing in setState
373
+ *
374
+ * If possible, please consider updating your useState calls to useImmer and using the immer
375
+ * versions of these functions
376
+ *
377
+ * Use like so:
378
+ *
379
+ * [state, setState] = useState<{ num: number; string: string; }>({ num: 1, string: 'str' });
380
+ *
381
+ * const updateField = curriedInnerReactOuterReact(setState);
382
+ *
383
+ * updateField('num', 3); // Updates state to { num: 3, string: 'str' }
384
+ *
385
+ * updateField('num', oldNum => oldNum + 1); // Updates state to { num: 4, string: 'str' }
386
+ *
387
+ * updateField(setState, 'num', 'three'); // TS Error due to invalid type
388
+ */
389
+ export const curriedInnerReactOuterReact =
390
+ <State>(setState: SetState<State>): InnerReactOuterReact<State> =>
391
+ <
392
+ Field extends keyof State,
393
+ Value extends SetStateAction<State[Field]>,
394
+ ValueArg extends Value | undefined
395
+ >(
396
+ field: Field,
397
+ value?: ValueArg
398
+ ) =>
399
+ innerReactOuterReact<State, Field, Value, ValueArg>(setState, field, value as any) as any;
400
+
401
+ export function curriedStateSlice<State>(setState: Updater<State>): InnerImmerOuterImmer<State>;
402
+ export function curriedStateSlice<State>(
403
+ setState: Updater<State>,
404
+ options: { inner: 'immer'; outer: 'immer' }
405
+ ): InnerImmerOuterImmer<State>;
406
+ export function curriedStateSlice<State>(
407
+ setState: Updater<State>,
408
+ options: { inner: 'immer'; outer: 'react' }
409
+ ): InnerImmerOuterReact<State>;
410
+ export function curriedStateSlice<State>(
411
+ setState: SetState<State>,
412
+ options: { inner: 'react'; outer: 'immer' }
413
+ ): InnerReactOuterImmer<State>;
414
+ export function curriedStateSlice<State>(
415
+ setState: SetState<State>,
416
+ options: { inner: 'react'; outer: 'react' }
417
+ ): InnerReactOuterReact<State>;
418
+ /**
419
+ * This is a magical function that allows you to slice up complex state into smaller pieces of state
420
+ * that are completely contained.
421
+ *
422
+ * This function works with both useImmer and useState, and allows you to seamlessly transform
423
+ * between the two when creating state slices.
424
+ *
425
+ * @example
426
+ * ```tsx
427
+ * // This component will have access to the whole state, while dealing with subcomponents who only
428
+ * // want access to the individual pieces as if they were their own pieces of state
429
+ * type ComplexStateObject = {
430
+ * piece1: { num: number; str: string };
431
+ * piece2: { arr: number[]; obj: { num: number } };
432
+ * piece3: string;
433
+ * }
434
+ *
435
+ * const ComplexState = () => {
436
+ * [state, setState] = useImmer<ComplexStateObject>({
437
+ * piece1: { num: 1, str: '' },
438
+ * piece2: { arr: [], obj: { num: 2 } },
439
+ * piece3: '',
440
+ * });
441
+ *
442
+ * const updateSlice = curriedStateSlice(setState);
443
+ *
444
+ * return (
445
+ * <>
446
+ * <ComponentWantingOnlyPiece1
447
+ * state={state.piece1}
448
+ * setState={updateSlice('piece1')}
449
+ * />
450
+ * <ComponentWantingOnlyPiece2
451
+ * state={state.piece2}
452
+ * setState={updateSlice('piece2')}
453
+ * />
454
+ * <input
455
+ * type="text"
456
+ * onChange={e => updateSlice('piece3', e.target.value)}
457
+ * value={state.piece3}
458
+ * />
459
+ * </>
460
+ * )
461
+ * }
462
+ * ```
463
+ *
464
+ * The function returned by this function takes in a type-safe string representing a key from the
465
+ * state object, followed by a type-checked value for that key's field.
466
+ *
467
+ * ```ts
468
+ * // Assuming updateSlice is called in the above component
469
+ * updateSlice('piece4', 'nice'); // This errors because piece4 is not in our state object
470
+ * updateSlice('piece3', 123); // This errors because piece3 should be a string, not a number
471
+ * ```
472
+ *
473
+ * If you are not using immer everywhere (you really should be when dealing with complex state),
474
+ * you can pass in an object as the second parameter specifying whether you want to use immer or
475
+ * react's setState functions for both your outer and inner state objects
476
+ *
477
+ * If you have any questions about this guy, please send a scribbled note via carrier pigeon to T2!
478
+ */
479
+ export function curriedStateSlice<
480
+ State,
481
+ Inner extends ImmerOrReact = 'immer',
482
+ Outer extends ImmerOrReact = 'immer'
483
+ >(
484
+ setState: Updater<State> | SetState<State>,
485
+ options?: { inner: Inner; outer: Outer }
486
+ ):
487
+ | InnerImmerOuterImmer<State>
488
+ | InnerReactOuterImmer<State>
489
+ | InnerImmerOuterReact<State>
490
+ | InnerReactOuterReact<State> {
491
+ // The type errors in this function can safely be ignored.
492
+ // For some reason, this type signature works for consumers of this function, but fails to
493
+ // correctly type-check the function body itself
494
+ if ((options?.outer ?? 'immer') === 'immer') {
495
+ return (options?.inner ?? 'immer') === 'immer'
496
+ ? curriedInnerImmerOuterImmer(setState as Updater<State>)
497
+ : curriedInnerReactOuterImmer(setState as Updater<State>);
498
+ }
499
+
500
+ return (options?.inner ?? 'immer') === 'immer'
501
+ ? curriedInnerImmerOuterReact(setState as SetState<State>)
502
+ : curriedInnerReactOuterReact(setState as SetState<State>);
503
+ }
504
+
505
+ type UpdateSlice<State> = {
506
+ (index: number): Updater<State>;
507
+ (index: number, value: UpdaterArgs<State>): void;
508
+ };
509
+
510
+ type CurriedArraySlice = {
511
+ <State>(setState: SetState<State[]>): UpdateSlice<State>;
512
+ <State>(setState: SetState<State[]>, index: number): Updater<State>;
513
+ <State>(setState: SetState<State[]>, index: number, value: State): void;
514
+ };
515
+
516
+ export const curriedArraySlice = (<State>(
517
+ setState: SetState<State[]>,
518
+ index?: number,
519
+ value?: State
520
+ ) => {
521
+ const updateSlice = ((innerIndex: number, innerValue?: State) => {
522
+ const update: Updater<State> = (reallyInnerValue: UpdaterArgs<State>) =>
523
+ setState(
524
+ produce((draft: Draft<State[]>) => {
525
+ if (typeof reallyInnerValue === 'function') {
526
+ (reallyInnerValue as DraftFunction<State>)(draft[innerIndex] as any);
527
+ } else {
528
+ draft[innerIndex] = castDraft(reallyInnerValue);
529
+ }
530
+ })
531
+ );
532
+
533
+ if (!innerValue) return update;
534
+
535
+ return update(innerValue);
536
+ }) as UpdateSlice<State>;
537
+
538
+ if (!index) return updateSlice;
539
+
540
+ if (!value) return updateSlice(index);
541
+
542
+ return updateSlice(index, value);
543
+ }) as CurriedArraySlice;
544
+
545
+ type ImmerArraySlice = {
546
+ <State>(setState: Updater<State[]>): UpdateSlice<State>;
547
+ <State>(setState: Updater<State[]>, index: number): Updater<State>;
548
+ <State>(setState: Updater<State[]>, index: number, value: State): void;
549
+ };
550
+
551
+ export const immerArraySlice = (<State>(
552
+ setState: Updater<State[]>,
553
+ index?: number,
554
+ value?: State
555
+ ) => {
556
+ const updateSlice = ((innerIndex: number, innerValue?: State) => {
557
+ const update: Updater<State> = (reallyInnerValue: UpdaterArgs<State>) =>
558
+ setState((oldState: Draft<State[]>) => {
559
+ if (typeof reallyInnerValue === 'function') {
560
+ (reallyInnerValue as DraftFunction<State>)(oldState[innerIndex] as any);
561
+ } else {
562
+ oldState[innerIndex] = castDraft(reallyInnerValue);
563
+ }
564
+ });
565
+
566
+ if (!innerValue) return update;
567
+
568
+ return update(innerValue);
569
+ }) as UpdateSlice<State>;
570
+
571
+ if (!index) return updateSlice;
572
+
573
+ if (!value) return updateSlice(index);
574
+
575
+ return updateSlice(index, value) as any;
576
+ }) as ImmerArraySlice;
@@ -0,0 +1,4 @@
1
+ export const capitalizeFirstLetter = (string: string) => {
2
+ if (!string) return string;
3
+ return string.charAt(0).toUpperCase() + string.slice(1);
4
+ };
@@ -0,0 +1,5 @@
1
+ export const isNotMaybe = <T>(value?: T | null | undefined): value is T =>
2
+ value !== undefined && value !== null;
3
+
4
+ export const filterMaybes = <T>(array?: (T | null | undefined)[]): T[] =>
5
+ array?.filter(isNotMaybe) ?? [];