@ngrx/signals 21.0.0 → 21.1.0

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.
@@ -57,10 +57,63 @@ function isIterable(value) {
57
57
  return typeof value?.[Symbol.iterator] === 'function';
58
58
  }
59
59
 
60
+ /**
61
+ * @description
62
+ *
63
+ * Creates a computed signal with deeply nested signals for each property when
64
+ * the result is an object literal.
65
+ *
66
+ * @usageNotes
67
+ *
68
+ * ```ts
69
+ * import { signal } from '@angular/core';
70
+ * import { deepComputed } from '@ngrx/signals';
71
+ *
72
+ * const limit = signal(10);
73
+ * const offset = signal(0);
74
+ *
75
+ * const pagination = deepComputed(() => ({
76
+ * currentPage: Math.floor(offset() / limit()) + 1,
77
+ * pageSize: limit(),
78
+ * }));
79
+ *
80
+ * console.log(pagination()); // { currentPage: 1, pageSize: 10 }
81
+ * console.log(pagination.currentPage()); // 1
82
+ * console.log(pagination.pageSize()); // 10
83
+ * ```
84
+ */
60
85
  function deepComputed(computation) {
61
86
  return toDeepSignal(computed(computation));
62
87
  }
63
88
 
89
+ /**
90
+ * @description
91
+ *
92
+ * Creates a method for managing side effects with signals.
93
+ * The method accepts a signal, a computation function, or a static value.
94
+ *
95
+ * @usageNotes
96
+ *
97
+ * ```ts
98
+ * import { Component, signal } from '@angular/core';
99
+ * import { signalMethod } from '@ngrx/signals';
100
+ *
101
+ * \@Component(...)
102
+ * export class Counter {
103
+ * readonly count = signal(1);
104
+ * readonly logDoubledNumber = signalMethod<number>(
105
+ * (num) => console.log(num * 2)
106
+ * );
107
+ *
108
+ * constructor() {
109
+ * this.logDoubledNumber(10); // logs: 20
110
+ *
111
+ * this.logDoubledNumber(this.count); // logs: 2
112
+ * setTimeout(() => this.count.set(2), 1_000); // logs: 4 (after 1s)
113
+ * }
114
+ * }
115
+ * ```
116
+ */
64
117
  function signalMethod(processingFn, config) {
65
118
  if (typeof ngDevMode !== 'undefined' && ngDevMode && !config?.injector) {
66
119
  assertInInjectionContext(signalMethod);
@@ -74,7 +127,7 @@ function signalMethod(processingFn, config) {
74
127
  ngDevMode &&
75
128
  config?.injector === undefined &&
76
129
  callerInjector === undefined) {
77
- console.warn('@ngrx/signals: The function returned by signalMethod was called', 'outside the injection context with a signal. This may lead to', 'a memory leak. Make sure to call it within the injection context', '(e.g. in a constructor or field initializer) or pass an injector', 'explicitly via the config parameter.\n\nFor more information, see:', 'https://ngrx.io/guide/signals/signal-method#automatic-cleanup');
130
+ console.warn('@ngrx/signals: Calling signalMethod outside of an injection', 'context with a signal is deprecated. In a future version,', 'this will throw an error. Either call it within an injection', 'context (e.g. in a constructor or field initializer) or pass', 'an injector explicitly via the config parameter.', '\n\nFor more information, see:', 'https://ngrx.io/guide/signals/signal-method#automatic-cleanup');
78
131
  }
79
132
  const instanceInjector = config?.injector ?? callerInjector ?? sourceInjector;
80
133
  const watcher = effect(() => {
@@ -125,6 +178,30 @@ function isWritableStateSource(stateSource) {
125
178
  return isWritableSignal(signals[key]);
126
179
  });
127
180
  }
181
+ /**
182
+ * @description
183
+ *
184
+ * Updates the state of a SignalStore or SignalState.
185
+ * Accepts a sequence of partial state objects and partial state updaters.
186
+ *
187
+ * @usageNotes
188
+ *
189
+ * ```ts
190
+ * import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
191
+ *
192
+ * export const CounterStore = signalStore(
193
+ * withState({ count1: 0, count2: 0 }),
194
+ * withMethods((store) => ({
195
+ * incrementFirst(): void {
196
+ * patchState(store, (state) => ({ count1: state.count1 + 1 }));
197
+ * },
198
+ * resetSecond(): void {
199
+ * patchState(store, { count2: 0 });
200
+ * },
201
+ * }))
202
+ * );
203
+ * ```
204
+ */
128
205
  function patchState(stateSource, ...updaters) {
129
206
  const currentState = untracked(() => getState(stateSource));
130
207
  const newState = updaters.reduce((nextState, updater) => ({
@@ -146,6 +223,36 @@ function patchState(stateSource, ...updaters) {
146
223
  }
147
224
  notifyWatchers(stateSource);
148
225
  }
226
+ /**
227
+ * @description
228
+ *
229
+ * Returns a snapshot of the current state from a SignalStore or SignalState.
230
+ * When used within a reactive context, state changes are automatically tracked.
231
+ *
232
+ * @usageNotes
233
+ *
234
+ * ```ts
235
+ * import { Component, effect, inject } from '@angular/core';
236
+ * import { getState, signalStore, withState } from '@ngrx/signals';
237
+ *
238
+ * export const CounterStore = signalStore(
239
+ * withState({ count1: 0, count2: 0 })
240
+ * );
241
+ *
242
+ * \@Component(...)
243
+ * export class Counter {
244
+ * readonly store = inject(CounterStore);
245
+ *
246
+ * constructor() {
247
+ * effect(() => {
248
+ * const state = getState(this.store);
249
+ * // 👇 Logs on state changes.
250
+ * console.log(state);
251
+ * });
252
+ * }
253
+ * }
254
+ * ```
255
+ */
149
256
  function getState(stateSource) {
150
257
  const signals = stateSource[STATE_SOURCE];
151
258
  return Reflect.ownKeys(stateSource[STATE_SOURCE]).reduce((state, key) => {
@@ -156,6 +263,28 @@ function getState(stateSource) {
156
263
  };
157
264
  }, {});
158
265
  }
266
+ /**
267
+ * @description
268
+ *
269
+ * Synchronously tracks state changes of a SignalStore or SignalState.
270
+ *
271
+ * @usageNotes
272
+ *
273
+ * ```ts
274
+ * import { Component } from '@angular/core';
275
+ * import { signalState, watchState } from '@ngrx/signals';
276
+ *
277
+ * \@Component(...)
278
+ * export class Counter {
279
+ * readonly state = signalState({ count1: 0, count2: 0 });
280
+ *
281
+ * constructor() {
282
+ * // 👇 Synchronously logs every state change without debouncing.
283
+ * watchState(this.state, console.log);
284
+ * }
285
+ * }
286
+ * ```
287
+ */
159
288
  function watchState(stateSource, watcher, config) {
160
289
  if (typeof ngDevMode !== 'undefined' && ngDevMode && !config?.injector) {
161
290
  assertInInjectionContext(watchState);
@@ -187,6 +316,32 @@ function removeWatcher(stateSource, watcher) {
187
316
  STATE_WATCHERS.set(stateSource[STATE_SOURCE], watchers.filter((w) => w !== watcher));
188
317
  }
189
318
 
319
+ /**
320
+ * @description
321
+ *
322
+ * Creates a state container with deeply nested signals for each property that
323
+ * is an object literal.
324
+ *
325
+ * @usageNotes
326
+ *
327
+ * ```ts
328
+ * import { Component } from '@angular/core';
329
+ * import { signalState, patchState } from '@ngrx/signals';
330
+ *
331
+ * \@Component(...)
332
+ * export class Counter {
333
+ * readonly state = signalState({ count: 0 });
334
+ *
335
+ * logCount(): void {
336
+ * console.log(this.state.count());
337
+ * }
338
+ *
339
+ * increment(): void {
340
+ * patchState(this.state, ({ count }) => ({ count: count + 1 }));
341
+ * }
342
+ * }
343
+ * ```
344
+ */
190
345
  function signalState(initialState) {
191
346
  const stateKeys = Reflect.ownKeys(initialState);
192
347
  const stateSource = stateKeys.reduce((signalsDict, key) => ({
@@ -205,6 +360,44 @@ function signalState(initialState) {
205
360
  return signalState;
206
361
  }
207
362
 
363
+ /**
364
+ * @description
365
+ *
366
+ * Creates a store by composing features.
367
+ * Returns an injectable service that can be provided locally or globally.
368
+ *
369
+ * @usageNotes
370
+ *
371
+ * ```ts
372
+ * import { Component, inject } from '@angular/core';
373
+ * import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
374
+ *
375
+ * export const CounterStore = signalStore(
376
+ * withState({ count: 0 }),
377
+ * withMethods((store) => ({
378
+ * increment(): void {
379
+ * patchState(store, ({ count }) => ({ count: count + 1 }));
380
+ * },
381
+ * }))
382
+ * );
383
+ *
384
+ * \@Component({
385
+ * // ...
386
+ * providers: [CounterStore],
387
+ * })
388
+ * export class Counter {
389
+ * readonly store = inject(CounterStore);
390
+ *
391
+ * logCount(): void {
392
+ * console.log(this.store.count());
393
+ * }
394
+ *
395
+ * increment(): void {
396
+ * this.store.increment();
397
+ * }
398
+ * }
399
+ * ```
400
+ */
208
401
  function signalStore(...args) {
209
402
  const signalStoreArgs = [...args];
210
403
  const config = typeof signalStoreArgs[0] === 'function'
@@ -251,6 +444,36 @@ function getInitialInnerStore() {
251
444
  };
252
445
  }
253
446
 
447
+ /**
448
+ * @description
449
+ *
450
+ * Combines multiple store features into a single feature.
451
+ *
452
+ * @usageNotes
453
+ *
454
+ * ```ts
455
+ * import {
456
+ * patchState,
457
+ * signalStore,
458
+ * signalStoreFeature,
459
+ * withMethods,
460
+ * withState,
461
+ * } from '@ngrx/signals';
462
+ *
463
+ * export function withCounter() {
464
+ * return signalStoreFeature(
465
+ * withState({ count: 0 }),
466
+ * withMethods((store) => ({
467
+ * increment(): void {
468
+ * patchState(store, ({ count }) => ({ count: count + 1 }));
469
+ * },
470
+ * }))
471
+ * );
472
+ * }
473
+ *
474
+ * export const CounterStore = signalStore(withCounter());
475
+ * ```
476
+ */
254
477
  function signalStoreFeature(...args) {
255
478
  const features = (typeof args[0] === 'function' ? args : args.slice(1));
256
479
  return (inputStore) => features.reduce((store, feature) => feature(store), inputStore);
@@ -271,6 +494,25 @@ function assertUniqueStoreMembers(store, newMemberKeys) {
271
494
  }
272
495
  }
273
496
 
497
+ /**
498
+ * @description
499
+ *
500
+ * Adds custom properties to a SignalStore.
501
+ *
502
+ * @usageNotes
503
+ *
504
+ * ```ts
505
+ * import { toObservable } from '@angular/core/rxjs-interop';
506
+ * import { signalStore, withProps, withState } from '@ngrx/signals';
507
+ *
508
+ * export const TodosStore = signalStore(
509
+ * withState({ todos: [] as Todo[], isLoading: false }),
510
+ * withProps(({ isLoading }) => ({
511
+ * isLoading$: toObservable(isLoading),
512
+ * }))
513
+ * );
514
+ * ```
515
+ */
274
516
  function withProps(propsFactory) {
275
517
  return (store) => {
276
518
  const props = propsFactory({
@@ -289,6 +531,26 @@ function withProps(propsFactory) {
289
531
  };
290
532
  }
291
533
 
534
+ /**
535
+ * @description
536
+ *
537
+ * Adds computed signals to a SignalStore.
538
+ * Accepts a factory function that returns a dictionary of computed signals or
539
+ * computation functions.
540
+ *
541
+ * @usageNotes
542
+ *
543
+ * ```ts
544
+ * import { signalStore, withState, withComputed } from '@ngrx/signals';
545
+ *
546
+ * export const CounterStore = signalStore(
547
+ * withState({ count: 0 }),
548
+ * withComputed(({ count }) => ({
549
+ * doubleCount: () => count() * 2,
550
+ * }))
551
+ * );
552
+ * ```
553
+ */
292
554
  function withComputed(computedFactory) {
293
555
  return withProps((store) => {
294
556
  const computedResult = computedFactory(store);
@@ -307,25 +569,27 @@ function withComputed(computedFactory) {
307
569
 
308
570
  /**
309
571
  * @description
310
- * Allows passing properties, methods, or signals from a SignalStore
311
- * to a feature.
572
+ *
573
+ * Allows passing state signals, properties, and methods from a SignalStore
574
+ * instance to a custom feature.
312
575
  *
313
576
  * @usageNotes
314
- * ```typescript
315
- * signalStore(
577
+ *
578
+ * ```ts
579
+ * import { signalStore, withFeature, withMethods } from '@ngrx/signals';
580
+ *
581
+ * export const UserStore = signalStore(
316
582
  * withMethods((store) => ({
317
- * load(id: number): Observable<Entity> {
318
- * return of({ id, name: 'John' });
583
+ * loadById(id: number): Promise<User> {
584
+ * return Promise.resolve({ id, name: 'John' });
319
585
  * },
320
586
  * })),
321
587
  * withFeature(
322
- * // 👇 has full access to the store
323
- * (store) => withEntityLoader((id) => firstValueFrom(store.load(id)))
588
+ * // 👇 Has full access to store members.
589
+ * (store) => withEntityLoader((id) => store.loadById(id))
324
590
  * )
325
591
  * );
326
592
  * ```
327
- *
328
- * @param featureFactory function returning the actual feature
329
593
  */
330
594
  function withFeature(featureFactory) {
331
595
  return (store) => {
@@ -339,6 +603,31 @@ function withFeature(featureFactory) {
339
603
  };
340
604
  }
341
605
 
606
+ /**
607
+ * @description
608
+ *
609
+ * Adds lifecycle hooks to a SignalStore.
610
+ * Supports an onInit hook that executes when the store is initialized.
611
+ * Supports an onDestroy hook for when the store is destroyed.
612
+ *
613
+ * @usageNotes
614
+ *
615
+ * ```ts
616
+ * import { signalStore, withHooks, withState } from '@ngrx/signals';
617
+ *
618
+ * export const UserStore = signalStore(
619
+ * withState({ firstName: 'Jimi', lastName: 'Hendrix' }),
620
+ * withHooks({
621
+ * onInit({ firstName }) {
622
+ * console.log('first name on init', firstName());
623
+ * },
624
+ * onDestroy({ lastName }) {
625
+ * console.log('last name on destroy', lastName());
626
+ * },
627
+ * })
628
+ * );
629
+ * ```
630
+ */
342
631
  function withHooks(hooksOrFactory) {
343
632
  return (store) => {
344
633
  const storeMembers = {
@@ -374,11 +663,17 @@ function withHooks(hooksOrFactory) {
374
663
  * @description
375
664
  *
376
665
  * Adds linked state slices to a SignalStore.
666
+ * Accepts a factory function that returns a dictionary of linked signals or
667
+ * computation functions.
377
668
  *
378
669
  * @usageNotes
379
670
  *
380
- * ```typescript
381
- * const OptionsStore = signalStore(
671
+ * ### Using a computation function
672
+ *
673
+ * ```ts
674
+ * import { signalStore, withLinkedState, withState } from '@ngrx/signals';
675
+ *
676
+ * export const OptionsStore = signalStore(
382
677
  * withState({ options: [1, 2, 3] }),
383
678
  * withLinkedState(({ options }) => ({
384
679
  * selectedOption: () => options()[0],
@@ -386,15 +681,15 @@ function withHooks(hooksOrFactory) {
386
681
  * );
387
682
  * ```
388
683
  *
389
- * This returns a state of type `{ options: number[], selectedOption: number | undefined }`.
390
- * When the `options` signal changes, the `selectedOption` automatically updates.
684
+ * ### Using linkedSignal for advanced use cases
391
685
  *
392
- * For advanced use cases, `linkedSignal` or any other `WritableSignal` instance can be used within `withLinkedState`:
686
+ * ```ts
687
+ * import { linkedSignal } from '@angular/core';
688
+ * import { signalStore, withLinkedState, withState } from '@ngrx/signals';
393
689
  *
394
- * ```typescript
395
690
  * type Option = { id: number; label: string };
396
691
  *
397
- * const OptionsStore = signalStore(
692
+ * export const OptionsStore = signalStore(
398
693
  * withState({ options: [] as Option[] }),
399
694
  * withLinkedState(({ options }) => ({
400
695
  * selectedOption: linkedSignal<Option[], Option>({
@@ -403,12 +698,10 @@ function withHooks(hooksOrFactory) {
403
698
  * const option = newOptions.find((o) => o.id === previous?.value.id);
404
699
  * return option ?? newOptions[0];
405
700
  * },
406
- * })
701
+ * }),
407
702
  * }))
408
703
  * )
409
704
  * ```
410
- *
411
- * @param linkedStateFactory A function that returns an object literal with properties containing an actual `linkedSignal` or the computation function.
412
705
  */
413
706
  function withLinkedState(linkedStateFactory) {
414
707
  return (store) => {
@@ -436,6 +729,29 @@ function withLinkedState(linkedStateFactory) {
436
729
  };
437
730
  }
438
731
 
732
+ /**
733
+ * @description
734
+ *
735
+ * Adds methods to a SignalStore.
736
+ *
737
+ * @usageNotes
738
+ *
739
+ * ```ts
740
+ * import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
741
+ *
742
+ * export const CounterStore = signalStore(
743
+ * withState({ count: 0 }),
744
+ * withMethods((store) => ({
745
+ * increment(): void {
746
+ * patchState(store, ({ count }) => ({ count: count + 1 }));
747
+ * },
748
+ * decrement(): void {
749
+ * patchState(store, ({ count }) => ({ count: count - 1 }));
750
+ * },
751
+ * }))
752
+ * );
753
+ * ```
754
+ */
439
755
  function withMethods(methodsFactory) {
440
756
  return (store) => {
441
757
  const methods = methodsFactory({
@@ -454,6 +770,22 @@ function withMethods(methodsFactory) {
454
770
  };
455
771
  }
456
772
 
773
+ /**
774
+ * @description
775
+ *
776
+ * Adds state slices to a SignalStore.
777
+ * Accepts an object or a factory function that returns the initial state.
778
+ *
779
+ * @usageNotes
780
+ *
781
+ * ```ts
782
+ * import { signalStore, withState } from '@ngrx/signals';
783
+ *
784
+ * export const CounterStore = signalStore(
785
+ * withState({ count: 0 })
786
+ * );
787
+ * ```
788
+ */
457
789
  function withState(stateOrFactory) {
458
790
  return (store) => {
459
791
  const state = (typeof stateOrFactory === 'function' ? stateOrFactory() : stateOrFactory);