reselect-kit 1.0.1 → 1.1.1

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.
Files changed (37) hide show
  1. package/README.md +307 -36
  2. package/dist/cjs/reselectKit.development.cjs +256 -95
  3. package/dist/cjs/reselectKit.development.cjs.map +1 -1
  4. package/dist/cjs/reselectKit.production.min.cjs +1 -1
  5. package/dist/cjs/reselectKit.production.min.cjs.map +1 -1
  6. package/dist/reselectKit.d.ts +201 -92
  7. package/dist/reselectKit.mjs +257 -97
  8. package/dist/reselectKit.mjs.map +1 -1
  9. package/package.json +6 -6
  10. package/src/__tests__/createAdaptedSelector.test.ts +3 -4
  11. package/src/__tests__/createBoundSelector.test.ts +98 -5
  12. package/src/__tests__/createChainSelector.bench.ts +117 -0
  13. package/src/__tests__/createChainSelector.test.ts +162 -6
  14. package/src/__tests__/createPathSelector.test.ts +14 -0
  15. package/src/__tests__/createSequenceSelector.test.ts +10 -14
  16. package/src/_helpers/__tests__/generateSelectorKey.test.ts +24 -0
  17. package/src/_helpers/__tests__/getSelectorName.test.ts +30 -0
  18. package/src/_helpers/unfreezeProps.ts +14 -0
  19. package/src/_hooks/__tests__/useSelector.bench.ts +133 -0
  20. package/src/_hooks/__tests__/useSelector.test.ts +69 -0
  21. package/src/_hooks/__tests__/useSelectors.bench.ts +160 -0
  22. package/src/_hooks/__tests__/useSelectors.test.ts +219 -0
  23. package/src/_hooks/selectorOptions.ts +11 -0
  24. package/src/_hooks/useSelector.ts +68 -26
  25. package/src/_hooks/useSelectors.ts +162 -0
  26. package/src/cache/__tests__/intervalMapCache.test.ts +11 -0
  27. package/src/createAdaptedSelector.ts +9 -5
  28. package/src/createBoundSelector.ts +92 -84
  29. package/src/createCachedSequenceSelector.ts +12 -8
  30. package/src/createCachedStructuredSelector.ts +11 -20
  31. package/src/createChainSelector.ts +134 -89
  32. package/src/createPathSelector.ts +71 -104
  33. package/src/createPropSelector.ts +9 -8
  34. package/src/createSequenceSelector.ts +14 -9
  35. package/src/index.ts +2 -0
  36. package/src/keys/createKeySelectorComposer.ts +12 -23
  37. package/src/types.ts +115 -7
package/README.md CHANGED
@@ -21,13 +21,15 @@ A powerful toolkit that extends the capabilities of @veksa/reselect and @veksa/r
21
21
  - **Bound Selectors** - Create selectors with predefined parameters for reuse across components
22
22
  - **Adapted Selectors** - Transform selector parameters to simplify composition and reuse
23
23
  - **Enhanced Structured Selectors** - Better typed alternatives to createStructuredSelector with caching support
24
+ - **Batched Store Reads** - Read a whole group of selectors through a single store subscription with `useSelectors`
24
25
  - **Key Selector Composition** - Compose key selectors for complex cache keys in re-reselect
25
26
  - **Custom Caching** - Optimize performance with advanced garbage collection and cache strategies
26
27
  - **TypeScript Support** - Full TypeScript type definitions with strict typing
27
28
 
28
29
  ## Installation
29
30
 
30
- reselect-kit requires **TypeScript 5.8 or later**.
31
+ reselect-kit requires **TypeScript 5.4 or later** — the floor the type tests run
32
+ against on every commit, together with every minor up to 7.0.
31
33
 
32
34
  ### Using npm or yarn
33
35
 
@@ -39,6 +41,37 @@ npm install reselect-kit
39
41
  yarn add reselect-kit
40
42
  ```
41
43
 
44
+ ### Upgrading from 1.0.x
45
+
46
+ Props are no longer a type parameter of their own: a selector is typed by the
47
+ tuple of arguments it takes after `state`, and a parametric selector is simply
48
+ one whose tuple is not empty. Call sites that let the types be inferred are
49
+ unaffected. Ones that spell the type arguments out move the props into the tuple:
50
+
51
+ ```typescript
52
+ // before
53
+ useSelector<State, PersonProps, Person>(getPerson, { personId: 1 });
54
+ createPathSelector<State, Person, PersonProps>(getPerson);
55
+ createChainSelector<State, PersonProps, Person>(getPerson);
56
+
57
+ // after
58
+ useSelector<State, Person, [PersonProps]>(getPerson, { personId: 1 });
59
+ createPathSelector<State, Person, [PersonProps]>(getPerson);
60
+ createChainSelector<State, Person, [PersonProps]>(getPerson);
61
+ ```
62
+
63
+ `createCachedSequenceSelector` infers everything from the array it is given, so
64
+ its type arguments cannot be written out at all — annotate `state` in the
65
+ selectors instead.
66
+
67
+ Two more consequences of the same change:
68
+
69
+ - a sequence returns a tuple of the individual result types rather than `R[]`,
70
+ which is what makes each position keep its own type;
71
+ - a chain step that reads props now makes them required for the whole built
72
+ selector even when the base selector had none. That type used to be dropped,
73
+ so this surfaces as a new error on code that was already passing the props.
74
+
42
75
  ## Examples
43
76
 
44
77
  ### Comparing @veksa/reselect, @veksa/re-reselect, and reselect-kit
@@ -101,11 +134,11 @@ import { createPathSelector } from 'reselect-kit';
101
134
  const getUser = createPathSelector((state) => state.user);
102
135
 
103
136
  // For parametric selectors
104
- const getUserByUserId = createPathSelector((state, userId) => state.users?.[userId]);
137
+ const getUserByUserId = createPathSelector((state, props) => state.users?.[props.userId]);
105
138
 
106
- // Usage:
107
- const address = getUser(state).address(); // Safely returns undefined if path is broken
108
- const userAddress = getUserByUserId(state, '123').address(); // With parameters
139
+ // Walking the path builds a selector; calling it reads the store
140
+ const address = getUser.address()(state); // Safely returns undefined if path is broken
141
+ const userAddress = getUserByUserId.address()(state, { userId: '123' }); // With parameters
109
142
  ```
110
143
 
111
144
  reselect-kit provides safer property access with path selectors that handle null/undefined values automatically.
@@ -186,13 +219,14 @@ const getUserByNameAndRole = createSelector(
186
219
  (users, userName, userRole) => users[userName][userRole],
187
220
  );
188
221
 
189
- const getAdmin = createAdaptedSelector(getUserByNameAndRole, (props) => ({
190
- userName: props.userName,
222
+ const getAdmin = createAdaptedSelector(getUserByNameAndRole, (props: { name: string }) => ({
223
+ userName: props.name,
191
224
  userRole: 'admin',
192
225
  }));
193
226
 
194
- // Usage:
195
- const admin = getAdmin(state); // Same as getUserByName(state, {userName: 'admin'})
227
+ // Usage: the adapted selector takes the props the mapping accepts
228
+ const admin = getAdmin(state, { name: 'alice' });
229
+ // Same as getUserByNameAndRole(state, {userName: 'alice', userRole: 'admin'})
196
230
  ```
197
231
 
198
232
  #### Chain Selectors
@@ -223,17 +257,46 @@ import { createCachedStructuredSelector } from 'reselect-kit';
223
257
  const getUserProfile = createCachedStructuredSelector({
224
258
  name: (state) => state.user.name,
225
259
  email: (state) => state.user.email,
226
- address: createPathSelector(
227
- (state) => state.user,
228
- (user) => user?.contact?.address,
229
- ),
230
- })((state, userId) => userId);
260
+ address: createPathSelector((state) => state.user).contact.address(),
261
+ })({
262
+ keySelector: (state, props) => props.userId,
263
+ });
231
264
 
232
265
  // Usage:
233
- const profile = getUserProfile(state, '123');
266
+ const profile = getUserProfile(state, { userId: '123' });
234
267
  // Returns: { name: 'John', email: 'john@example.com', address: { ... } }
235
268
  ```
236
269
 
270
+ ### Reading a Batch of Selectors in a Component
271
+
272
+ The same structural idea at the call site: `useSelectors` takes an object of
273
+ selectors and reads them all through one store subscription.
274
+
275
+ ```typescript
276
+ import { useSelectors } from 'reselect-kit';
277
+
278
+ const QuoteRow = ({ symbol }) => {
279
+ const { bid, ask, volume } = useSelectors({ bid: getBid, ask: getAsk, volume: getVolume }, { symbol });
280
+
281
+ return <Row bid={bid} ask={ask} volume={volume} />;
282
+ };
283
+ ```
284
+
285
+ Which of the two to use is a question of where the memoization should live, not
286
+ of speed — they perform the same:
287
+
288
+ | | `createCachedStructuredSelector` | `useSelectors` |
289
+ | ------------ | --------------------------------------------- | -------------------------------- |
290
+ | What it is | a selector: composable, usable outside React | a read at the call site only |
291
+ | Memoization | one module-level cache, keyed | one slot per component instance |
292
+ | Cache key | a key selector is required | none |
293
+ | Cache sizing | must hold every live row, or it thrashes | dies with the component |
294
+ | Sharing | two components with the same key compute once | every component computes its own |
295
+
296
+ Reuse the batch elsewhere, or feed it into another selector — build a structured
297
+ selector. If the batch is simply "what this component reads", reach for the hook
298
+ and skip the key selector and the cache sizing.
299
+
237
300
  ### Key Selector Composition
238
301
 
239
302
  ```typescript
@@ -269,13 +332,19 @@ createChainSelector(baseSelector).map(transformFn).build();
269
332
  Transforms the output of the previous selector by creating a new selector based on its result. This is where the real power of chain selectors comes in, allowing composition of selectors where the output of one becomes the context for creating the next.
270
333
 
271
334
  ```typescript
272
- chain<S2, R2>(fn: (result: R1) => Selector<S2, R2>, options?: ChainSelectorOptions): SelectorMonad
273
- chain<S2, P2, R2>(fn: (result: R1) => Selector<S2, R2, [P2]>, options?: ChainSelectorOptions): SelectorMonad
335
+ chain<S2, R2, Params2>(
336
+ fn: (result: R1) => (state: S2, ...params: Params2) => R2,
337
+ options?: ChainSelectorOptions,
338
+ ): ChainSelectorResult
274
339
  ```
275
340
 
276
341
  - **fn** - Function that receives the previous selector's result and returns a new selector
277
342
  - **options** - Optional configuration for caching and key selection
278
343
 
344
+ A step reading props adds them to what the built selector requires, whether or
345
+ not the base selector had any: the arguments of every level are merged rather
346
+ than the level having to match the base.
347
+
279
348
  **Examples:**
280
349
 
281
350
  ```typescript
@@ -319,6 +388,29 @@ Transforms the output of the selector with a simple transformation function. Unl
319
388
  map<R2>(fn: (result: R1) => R2, options?: ChainSelectorOptions): SelectorMonad
320
389
  ```
321
390
 
391
+ ###### Keyed levels
392
+
393
+ A level only gets a keyed cache when its key can actually vary. Where the key is
394
+ a constant, the cache would hold a single entry for the lifetime of the chain
395
+ while every call still paid for the key computation and the lookup guarding it,
396
+ so the level is built as a plain memoized selector instead — the same mapping
397
+ without the layer.
398
+
399
+ What each level knows about its key differs:
400
+
401
+ - a `map` level derives a plain `() => output`, never a cached selector, so its
402
+ key is its base's own and is composed once at build time. A chain of `map`s
403
+ over a plain (uncached) selector composes to the default key, and the built
404
+ selector then carries neither `keySelector` nor `cache`;
405
+ - a `chain` level cannot know its key at build time, because the selector it
406
+ derives exists only per call and may itself be cached — chaining a bound
407
+ cached selector off a plain base is the ordinary case. So it always carries a
408
+ `keySelector` that resolves the derived selector's key. Over an uncached base
409
+ that key evaluates to the default, but it is not the default _selector_, so a
410
+ level built on top of it does take the keyed path and holds a single entry.
411
+
412
+ Chaining off a cached selector is where those caches earn their keep.
413
+
322
414
  ###### build
323
415
 
324
416
  Completes the chain and returns the final selector function that can be used in components.
@@ -329,43 +421,56 @@ build(): Selector<S, R, Params>
329
421
 
330
422
  #### createPathSelector
331
423
 
332
- Creates a selector that safely accesses nested properties.
424
+ Creates a selector that safely accesses nested properties. Property access walks
425
+ the path and the call terminates it, producing the selector.
333
426
 
334
427
  ```typescript
335
- createPathSelector(sourceSelector, pathFn);
428
+ createPathSelector(sourceSelector).some.nested.path();
336
429
  ```
337
430
 
338
431
  #### createBoundSelector
339
432
 
340
- Creates a selector with predefined parameters.
433
+ Creates a selector with predefined props. Whatever the binding does not supply
434
+ stays required; a binding covering every prop leaves a plain selector.
341
435
 
342
436
  ```typescript
343
- createBoundSelector(selector, getParams);
437
+ createBoundSelector(selector, binding);
344
438
  ```
345
439
 
346
440
  #### createAdaptedSelector
347
441
 
348
- Adapts a selector to work with a different parameter shape.
442
+ Adapts a selector to work with a different props shape. The adapted selector
443
+ takes the props the mapping accepts, in place of the ones the base selector reads.
349
444
 
350
445
  ```typescript
351
- createAdaptedSelector(selector, paramsAdapter);
446
+ createAdaptedSelector(selector, propsAdapter);
352
447
  ```
353
448
 
354
449
  #### createCachedStructuredSelector
355
450
 
356
- Creates a structured selector with caching support.
451
+ Creates a structured selector with caching support. The second call takes either
452
+ re-reselect's options object or the key selector on its own.
357
453
 
358
454
  ```typescript
359
- createCachedStructuredSelector(selectors)(keySelector);
455
+ createCachedStructuredSelector(selectors)({ keySelector });
360
456
  ```
361
457
 
458
+ The key selector is typed from the batch, so its `state` and props are the ones
459
+ the batch reads.
460
+
461
+ The second call needs a way to derive the key: a `keySelector`, given on its own
462
+ or inside the options, or a `keySelectorCreator` that produces one. re-reselect
463
+ types both as optional, so an options object carrying neither compiles and then
464
+ throws on the first read.
465
+
362
466
  #### createEmptySelector
363
467
 
364
468
  Creates a selector that always returns `undefined` regardless of input. Useful as a placeholder or for conditional selection logic.
365
469
 
366
470
  ```typescript
367
471
  const emptySelector = createEmptySelector(baseSelector);
368
- // Always returns undefined while maintaining the original selector's signature
472
+ // Always returns undefined; the result widens to `R | undefined` and the
473
+ // selector takes whatever arguments it is called with
369
474
  ```
370
475
 
371
476
  #### createPropSelector
@@ -378,12 +483,7 @@ const propSelector = createPropSelector();
378
483
 
379
484
  // In a chain
380
485
  const userByIdSelector = createChainSelector(propSelector)
381
- .chain((props) =>
382
- createPathSelector(
383
- (state) => state.users[props.userId],
384
- (user) => user,
385
- ),
386
- )
486
+ .chain((props) => createPathSelector((state) => state.users[props.userId]).profile())
387
487
  .build();
388
488
  ```
389
489
 
@@ -400,7 +500,10 @@ const getUserSettings = createSegmentSelector(
400
500
 
401
501
  #### createSequenceSelector
402
502
 
403
- Creates a selector that returns an array of results from multiple selectors.
503
+ Runs a group of selectors and collects their results positionally. The result is
504
+ a tuple of the individual result types, and `state` and the extra arguments are
505
+ merged across the group, so a single parametric member makes props required for
506
+ the whole sequence.
404
507
 
405
508
  ```typescript
406
509
  const getUserStats = createSequenceSelector([
@@ -409,7 +512,163 @@ const getUserStats = createSequenceSelector([
409
512
  (state) => state.user.likesCount,
410
513
  ]);
411
514
 
412
- // Returns [postsCount, followersCount, likesCount]
515
+ // getUserStats(state) returns [postsCount, followersCount, likesCount]
516
+ ```
517
+
518
+ #### createCachedSequenceSelector
519
+
520
+ The cached counterpart of `createSequenceSelector`: the second call takes
521
+ re-reselect's options, with the key selector typed from the group.
522
+
523
+ ```typescript
524
+ const getPostStats = createCachedSequenceSelector([
525
+ (state, props) => state.posts[props.postId].likes,
526
+ (state, props) => state.posts[props.postId].comments,
527
+ ])({
528
+ keySelector: (state, props) => props.postId,
529
+ });
530
+
531
+ // getPostStats(state, { postId: 1 }) returns [likes, comments]
532
+ ```
533
+
534
+ ### Hooks
535
+
536
+ #### useSelector
537
+
538
+ Reads one selector from the store. Props are compared shallowly, so an inline
539
+ object literal does not invalidate the selector on every render.
540
+
541
+ ```typescript
542
+ const price = useSelector(getPrice, { symbol });
543
+ ```
544
+
545
+ #### useSelectors
546
+
547
+ Reads a batch of selectors through a **single** store subscription and returns
548
+ their results as one object.
549
+
550
+ ```typescript
551
+ const { symbol, bid, ask } = useSelectors(
552
+ { symbol: getSymbol, bid: getBid, ask: getAsk },
553
+ { symbol },
554
+ );
555
+ ```
556
+
557
+ The props argument follows the batch: it is rejected when no selector takes
558
+ props, required as soon as one requires them, and optional when every selector
559
+ that reads props declares them optional. The props of the batch are intersected,
560
+ so one object satisfies every member.
561
+
562
+ Every member is read as `selector(state, props)`, so a selector that requires a
563
+ third argument does not belong in a batch — the batch has nothing to pass for it
564
+ and it arrives as `undefined`.
565
+
566
+ Seven `useSelector` calls in a row component mean seven store listeners, seven
567
+ `useSyncExternalStore` instances and seven snapshot comparisons per dispatch;
568
+ `useSelectors` collapses them into one of each. The result object keeps its
569
+ reference while every field is unchanged, so an update that touches nothing the
570
+ row reads does not re-render it.
571
+
572
+ The trade-off against separate hooks is granularity: any tracked state segment
573
+ changing re-runs the whole batch, which for memoized selectors is a cache hit
574
+ per field. See [Reading a Batch of Selectors in a Component](#reading-a-batch-of-selectors-in-a-component)
575
+ for how this compares with a structured selector.
576
+
577
+ ### Key Selectors
578
+
579
+ #### stringComposeKeySelectors
580
+
581
+ Composes several key selectors into one that joins their keys with `:`. The
582
+ result carries its inputs as `dependencies`, which is what lets
583
+ `createBoundSelector` drop the key selectors whose props it has bound.
584
+
585
+ ```typescript
586
+ const keySelector = stringComposeKeySelectors(
587
+ (state, props) => props.userId,
588
+ (state, props) => props.view,
589
+ );
590
+
591
+ // keySelector(state, { userId: '123', view: 'details' }) returns '123:details'
592
+ ```
593
+
594
+ #### arrayComposeKeySelectors
595
+
596
+ The same composition, producing an array key instead of a string — for cache
597
+ objects that take multi-part keys, such as [TreeCache](#treecache). Nested array
598
+ keys are flattened.
599
+
600
+ ```typescript
601
+ const keySelector = arrayComposeKeySelectors(
602
+ (state, props) => props.userId,
603
+ (state, props) => props.view,
604
+ );
605
+
606
+ // keySelector(state, { userId: '123', view: 'details' }) returns ['123', 'details']
607
+ ```
608
+
609
+ #### createKeySelectorComposer
610
+
611
+ Builds a composer of your own from a function that reduces N key selectors into
612
+ one. Both composers above are built with it.
613
+
614
+ ```typescript
615
+ const commaComposeKeySelectors = createKeySelectorComposer(
616
+ (...keySelectors) =>
617
+ (state, props) =>
618
+ keySelectors.map((keySelector) => keySelector(state, props)).join(),
619
+ );
620
+ ```
621
+
622
+ #### createKeySelectorCreator
623
+
624
+ Builds the `keySelectorCreator` that re-reselect calls to derive a selector's key
625
+ from its inputs: it collects the key selectors of every cached input, flattens
626
+ already-composed ones, drops duplicates and the default key, and composes what is
627
+ left with the given composer.
628
+
629
+ ```typescript
630
+ const fullNameSelector = createCachedSelector(
631
+ [personByMessageIdSelector],
632
+ toFullName,
633
+ )({
634
+ keySelectorCreator: createKeySelectorCreator(stringComposeKeySelectors),
635
+ });
636
+ ```
637
+
638
+ A chain builds its own creator from the `keySelectorComposer` it was given, so
639
+ inside a chain the composer is what you configure, not the creator.
640
+
641
+ #### defaultKeySelector
642
+
643
+ The key of a selector that has nothing to key on — a single constant. Key
644
+ composition drops it, so a chain whose key cannot vary carries no cache at all.
645
+
646
+ #### ChainSelectorOptions
647
+
648
+ The options every chain level accepts, either on `createChainSelector` itself or
649
+ per `chain` / `map` call. A per-level object overrides what the chain was created
650
+ with, and the merged result carries to the levels after it.
651
+
652
+ ```typescript
653
+ type ChainSelectorOptions = {
654
+ // called per level, so each one gets its own cache object
655
+ createSelectorOptions?: () => { selectorCreator; cacheObject };
656
+ keySelectorComposer?: KeySelectorComposer;
657
+ };
658
+ ```
659
+
660
+ ### Debugging
661
+
662
+ #### setDebugMode / isDebugMode
663
+
664
+ Debug mode makes every creator tag the selectors it builds with a readable
665
+ `selectorName`, composed from the names of what they were built from. It is off
666
+ by default, since the names cost string work on every build.
667
+
668
+ ```typescript
669
+ import { setDebugMode } from 'reselect-kit';
670
+
671
+ setDebugMode(process.env.NODE_ENV !== 'production');
413
672
  ```
414
673
 
415
674
  ### Cache Objects
@@ -418,12 +677,17 @@ const getUserStats = createSequenceSelector([
418
677
 
419
678
  Implements a hierarchical cache structure for nested key support. Unlike flat cache objects, TreeCache allows for efficiently caching and retrieving selectors with complex, multi-part keys.
420
679
 
680
+ Pair it with [arrayComposeKeySelectors](#arraycomposekeyselectors), which is what
681
+ produces the multi-part keys it exists for.
682
+
421
683
  ```typescript
684
+ import { FlatObjectCache } from '@veksa/re-reselect';
422
685
  import { TreeCache } from 'reselect-kit';
423
686
 
424
- // Create a TreeCache instance
687
+ // The options object is required; `cacheObjectCreator` inside it is not, and
688
+ // defaults to a FlatObjectCache per level
425
689
  const cache = new TreeCache({
426
- cacheObjectCreator: () => new FlatObjectCache(), // Optional custom cache factory
690
+ cacheObjectCreator: () => new FlatObjectCache(),
427
691
  });
428
692
 
429
693
  // Use with complex keys (automatically normalized to arrays)
@@ -449,6 +713,11 @@ const cache = new IntervalMapCache();
449
713
  cache.set('key', selectorData);
450
714
  ```
451
715
 
716
+ The sweep runs on a 10-second interval and evicts entries untouched for that
717
+ long; reading an entry refreshes it. `initGarbageCollector` schedules through
718
+ `window`, so it is a no-op where there is none — on the server the cache simply
719
+ keeps what it is given. Calling it more than once does not stack collectors.
720
+
452
721
  ### Comparing with @veksa/reselect and @veksa/re-reselect
453
722
 
454
723
  | Feature | @veksa/reselect | @veksa/re-reselect | reselect-kit |
@@ -462,6 +731,8 @@ cache.set('key', selectorData);
462
731
  | Advanced key composition | × | Limited | ✓ |
463
732
  | Cache garbage collection | × | × | ✓ |
464
733
  | Hierarchical caching | × | × | ✓ |
734
+ | Props-aware React hooks | × | × | ✓ |
735
+ | Batched store subscription | × | × | ✓ |
465
736
  | Typescript support | ✓ | ✓ | ✓ (enhanced) |
466
737
 
467
738
  ## Contributing