### Basic Lifecycle Hooks with `withHooks` Source: https://ngrx.io/guide/signals/signal-store/lifecycle-hooks Use `onInit` to start a subscription that automatically unsubscribes on destroy. The `onDestroy` hook logs the store's state. ```typescript import { computed } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs'; import { patchState, signalStore, withState, withHooks, withMethods, } from '@ngrx/signals'; export const CounterStore = signalStore( withState({ count: 0 }), withMethods((store) => ({ increment(): void { patchState(store, (state) => ({ count: state.count + 1 })); }, })), withHooks({ onInit(store) { // 👇 Increment the `count` every 2 seconds. interval(2_000) // 👇 Automatically unsubscribe when the store is destroyed. .pipe(takeUntilDestroyed()) .subscribe(() => store.increment()); }, onDestroy(store) { console.log('count on destroy', store.count()); }, }) ); ``` -------------------------------- ### Manually Install NgRx Signals with npm Source: https://ngrx.io/guide/signals/install Install the `@ngrx/signals` package manually using npm. This is an alternative to the `ng add` command. ```bash npm install @ngrx/signals ``` -------------------------------- ### Named Entity Collection Setup Source: https://ngrx.io/guide/signals/signal-store/entity-management Sets up a store with a named entity collection, allowing for custom property names like 'todoIds' instead of 'ids'. ```typescript import { signalStore, type } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; type Todo = { id: number; text: string; completed: boolean; }; export const TodosStore = signalStore( // 💡 Entity type is specified using the `type` function. withEntities({ entity: type(), collection: 'todo' }) ); ``` -------------------------------- ### Install NgRx Signals with ng add Source: https://ngrx.io/guide/signals/install Use the `ng add` command to automatically install and configure the `@ngrx/signals` package in your Angular project. ```bash ng add @ngrx/signals@latest ``` -------------------------------- ### Use Implicitly Linked State in Component Source: https://ngrx.io/guide/signals/signal-store/linked-state This example shows how to inject and use the `OptionsStore` in a component, demonstrating how to access the `selectedOption` and update both `selectedOption` and `options` states. ```typescript @Component({ // ... other metadata providers: [OptionsStore], }) export class OptionList { readonly store = inject(OptionsStore); constructor() { console.log(this.store.selectedOption()); // logs: 1 this.store.setSelectedOption(2); console.log(this.store.selectedOption()); // logs: 2 this.store.setOptions([4, 5, 6]); console.log(this.store.selectedOption()); // logs: 4 } } ``` -------------------------------- ### Grouping Dependencies with `withProps` Source: https://ngrx.io/guide/signals/signal-store/custom-store-properties Group dependencies like services and loggers using `withProps` for easier management and reuse across multiple store features. This example injects `BooksService` and `Logger` and uses them within store methods and lifecycle hooks. ```typescript import { inject } from '@angular/core'; import { signalStore, withProps, withState } from '@ngrx/signals'; import { Logger } from './logger'; import { BooksService } from './books-service'; import { Book } from './book'; type BooksState = { books: Book[]; isLoading: boolean; }; export const BooksStore = signalStore( withState({ books: [], isLoading: false }), withProps(() => ({ booksService: inject(BooksService), logger: inject(Logger), })), withMethods(({ booksService, logger, ...store }) => ({ async loadBooks(): Promise { logger.debug('Loading books...'); patchState(store, { isLoading: true }); const books = await booksService.getAll(); logger.debug('Books loaded successfully', books); patchState(store, { books, isLoading: false }); }, })), withHooks({ onInit({ logger }) { logger.debug('BooksStore initialized'); }, }) ); ``` -------------------------------- ### Handle State Updates with Partial State Objects, Updaters, or Arrays Source: https://ngrx.io/guide/signals/signal-store/events This example shows how to use `withReducer` to handle events by returning different types of state updates: partial state objects, partial state updaters, or arrays of partial state objects and/or updaters. ```typescript const incrementBy = event( '[Counter Page] Increment By', type() ); const increment = event('[Counter Page] Increment'); const incrementBoth = event('[Counter Page] Increment Both'); export const CounterStore = signalStore( withState({ count1: 0, count2: 0 }), withReducer( // 👇 Returning a partial state object. on(incrementBy, (event, state) => ({ count1: state.count1 + event.payload, })), // 👇 Returning a partial state updater. on(increment, () => incrementFirst()), // 👇 Returning an array of partial state updaters. on(incrementBoth, () => [incrementFirst(), incrementSecond()]) ) ); function incrementFirst(): PartialStateUpdater<{ count1: number }> { return (state) => ({ count1: state.count1 + 1 }); } function incrementSecond(): PartialStateUpdater<{ count2: number }> { return (state) => ({ count2: state.count2 + 1 }); } ``` -------------------------------- ### Define a Reactive Method with rxMethod Source: https://ngrx.io/guide/signals/rxjs-integration Use `rxMethod` to create a reactive method that accepts RxJS operators. This example defines a method to log the doubled value of a number. ```typescript import { Component } from '@angular/core'; import { map, pipe, tap } from 'rxjs'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; @Component({ /* ... */ }) export class Numbers { // 👇 This reactive method will have an input argument // of type `number | (() => number) | Observable`. readonly logDoubledNumber = rxMethod( // 👇 RxJS operators are chained together using the `pipe` function. pipe( map((num) => num * 2), tap(console.log) ) ); } ``` -------------------------------- ### Define Event Handlers with Timer and Events Service Source: https://ngrx.io/guide/signals/signal-store/events Define event handlers using a combination of a timer observable and the `Events` service. This example demonstrates fetching all books every 30 seconds and logging errors when book loading fails. ```typescript // ... other imports import { exhaustMap, tap, timer } from 'rxjs'; import { withEventHandlers } from '@ngrx/signals/events'; import { mapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; export const BookSearchStore = signalStore( // ... other features withEventHandlers((store, booksService = inject(BooksService)) => [ timer(0, 30_000).pipe( exhaustMap(() => booksService.getAll().pipe( mapResponse({ next: (books) => booksApiEvents.loadedSuccess(books), error: (error: { message: string }) => booksApiEvents.loadedFailure(error.message), }) ) ) ), events .on(booksApiEvents.loadedFailure) .pipe(tap(({ payload }) => console.error(payload))), ]) ); ``` -------------------------------- ### Accessing Store Members in a Component Source: https://ngrx.io/guide/signals/signal-store/private-store-members This example shows how to inject and interact with a SignalStore in an Angular component. It highlights which members are accessible (public) and which are not (private). ```typescript import { Component, inject, OnInit } from '@angular/core'; import { CounterStore } from './counter-store'; @Component({ /* ... */ providers: [CounterStore], }) export class Counter implements OnInit { readonly store = inject(CounterStore); ngOnInit(): void { console.log(this.store.count1()); // ✅ console.log(this.store._count2()); // ❌ console.log(this.store._doubleCount1()); // ❌ console.log(this.store.doubleCount2()); // ✅ this.store._count2$.subscribe(console.log); // ❌ this.store.doubleCount1$.subscribe(console.log); // ✅ this.store.increment1(); // ✅ this.store._increment2(); // ❌ } } ``` -------------------------------- ### Define Reactive Store Method with rxMethod Source: https://ngrx.io/guide/signals/signal-store Use `rxMethod` to create a store method that leverages RxJS operators for handling asynchronous operations like debouncing input and switching observables. This example demonstrates loading books by a search query. ```typescript import { computed, inject } from '@angular/core'; import { debounceTime, distinctUntilChanged, pipe, switchMap, tap, } from 'rxjs'; import { patchState, signalStore /* ... */ } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; import { Book } from './book'; type BookSearchState = { /* ... */ }; const initialState: BookSearchState = { /* ... */ }; export const BookSearchStore = signalStore( withState(initialState), withComputed(/* ... */), withMethods((store, booksService = inject(BooksService)) => ({ /* ... */ // 👇 Defining a method to load books by query. loadByQuery: rxMethod( pipe( debounceTime(300), distinctUntilChanged(), tap(() => patchState(store, { isLoading: true })), switchMap((query) => { return booksService.getByQuery(query).pipe( tapResponse({ next: (books) => patchState(store, { books, isLoading: false }), error: (err) => { patchState(store, { isLoading: false }); console.error(err); }, }) ); }) ) ), })) ); ``` -------------------------------- ### Testing SignalStore State, Computed Values, and Methods Source: https://ngrx.io/guide/signals/signal-store/testing Test a SignalStore's initial state, derived computed values, and the effects of its methods. This example includes incrementing a count and verifying the updated state and computed doubleCount. ```typescript import { TestBed } from '@angular/core/testing'; import { patchState, signalStore, withComputed, withMethods, withState, } from '@ngrx/signals'; const CounterStore = signalStore( { providedIn: 'root' }, withState({ count: 0 }), withComputed(({ count }) => ({ doubleCount: () => count() * 2, })), withMethods((store) => ({ increment() { patchState(store, ({ count }) => ({ count: count + 1 })); }, })) ); // Test describe('CounterStore', () => { it('has an initial state and derived doubleCount', () => { const store = TestBed.inject(CounterStore); expect(store.count()).toBe(0); expect(store.doubleCount()).toBe(0); }); it('updates doubleCount when count changes on increment', () => { const store = TestBed.inject(CounterStore); store.increment(); expect(store.count()).toBe(1); expect(store.doubleCount()).toBe(2); store.increment(); expect(store.count()).toBe(2); expect(store.doubleCount()).toBe(4); }); }); ``` -------------------------------- ### Get SignalStore Type with InstanceType Source: https://ngrx.io/guide/signals/faq Shows how to obtain the type of a SignalStore using the `InstanceType` utility. This is useful for type safety when working with SignalStore instances. ```typescript const CounterStore = signalStore(withState({ count: 0 })); type CounterStore = InstanceType; function logCount(store: CounterStore): void { console.log(store.count()); } ``` -------------------------------- ### Define Private Entity Collections Source: https://ngrx.io/guide/signals/signal-store/entity-management Create private entity collections by prefixing the collection name with an underscore (`_`). This example shows how to define a private collection and expose its entities publicly using `withComputed`. ```typescript const todoConfig = entityConfig({ entity: type(), // 👇 private collection collection: '_todo', }); const TodosStore = signalStore( withEntities(todoConfig), withComputed(({ _todoEntities }) => ({ // 👇 exposing entity array publicly todos: _todoEntities, })) ); @Component({ /* ... */ template: `

Todos

`, providers: [TodosStore], }) class Todos { readonly store = inject(TodosStore); } ``` -------------------------------- ### Component Accessing Store State Source: https://ngrx.io/guide/signals/signal-store/events Components can access store state and computed signals directly using the store instance. This example shows a component reading query, loading status, and book list from the `BookSearchStore`. ```typescript import { ChangeDetectionStrategy, Component, inject, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BookSearchStore } from './book-search-store'; @Component({ selector: 'ngrx-book-search', imports: [FormsModule], template: `

Search Books

@if (store.isLoading()) {

Loading...

}
    @for (book of store.books(); track book.id) {
  • {{ book.title }}
  • }
`, providers: [BookSearchStore], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BookSearch { readonly store = inject(BookSearchStore); } ``` -------------------------------- ### signalMethod vs. Effect Example Source: https://ngrx.io/guide/signals/signal-method Compares the usage of `signalMethod` and `effect` for logging doubled numbers. `signalMethod` is called with a Signal value, while `effect` is defined with a computation function. ```typescript @Component({ /* ... */ }) export class Numbers { readonly num = signal(2); readonly logDoubledNumberEffect = effect(() => { console.log(this.num() * 2); }); readonly logDoubledNumber = signalMethod((num) => { console.log(num * 2); }); constructor() { this.logDoubledNumber(this.num); } } ``` -------------------------------- ### Fetch All Books using rxMethod (No Arguments) Source: https://ngrx.io/guide/signals/rxjs-integration Define a reactive method `loadAllBooks` without arguments using `rxMethod`. It uses `exhaustMap` to fetch all books and updates the `books` signal. Call it in the constructor to load books on initialization. ```typescript import { Component, inject, signal } from '@angular/core'; import { exhaustMap } from 'rxjs'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; import { Book } from './book'; @Component({ /* ... */ }) export class BookList { readonly #booksService = inject(BooksService); readonly books = signal([]); // 👇 Creating a reactive method without arguments. readonly loadAllBooks = rxMethod( exhaustMap(() => { return this.#booksService.getAll().pipe( tapResponse({ next: (books) => this.books.set(books), error: console.error, }) ); }) ); constructor() { this.loadAllBooks(); } } ``` -------------------------------- ### Creating a Custom Feature with Input using `withFeature` Source: https://ngrx.io/guide/signals/signal-store/custom-store-features Demonstrates how to create a reusable store feature that accepts external input, like a signal of books, and uses it to compute filtered results. This approach enhances flexibility by decoupling the feature from the store's internal state. ```typescript import { computed, Signal } from '@angular/core'; import { patchState, signalStore, signalStoreFeature, withComputed, withFeature, withMethods, withState, } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; import { Book } from './book'; export function withBooksFilter(books: Signal) { return signalStoreFeature( withState({ query: '' }), withComputed(({ query }) => ({ filteredBooks: computed(() => books().filter((b) => b.name.includes(query())) ), })), withMethods((store) => ({ setQuery(query: string): void { patchState(store, { query }); }, })) ); } export const BooksStore = signalStore( withEntities(), // 👇 Using `withFeature` to pass input to the `withBooksFilter` feature. withFeature(({ entities }) => withBooksFilter(entities)) ); ``` -------------------------------- ### BookSearchStore Implementation Source: https://ngrx.io/guide/signals/signal-store This snippet shows the complete implementation of a SignalStore for book searching, including state, computed signals, and methods for updating the query, order, and loading books. ```typescript import { computed, inject } from '@angular/core'; import { debounceTime, distinctUntilChanged, pipe, switchMap, tap, } from 'rxjs'; import { patchState, signalStore, withComputed, withMethods, withState, } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; import { Book } from './book'; type BookSearchState = { books: Book[]; isLoading: boolean; filter: { query: string; order: 'asc' | 'desc' }; }; const initialState: BookSearchState = { books: [], isLoading: false, filter: { query: '', order: 'asc' }, }; export const BookSearchStore = signalStore( withState(initialState), withComputed(({ books, filter }) => ({ booksCount: computed(() => books().length), sortedBooks: computed(() => { const direction = filter.order() === 'asc' ? 1 : -1; return books().toSorted( (a, b) => direction * a.title.localeCompare(b.title) ); }), })), withMethods((store, booksService = inject(BooksService)) => ({ updateQuery(query: string): void { patchState(store, (state) => ({ filter: { ...state.filter, query }, })); }, updateOrder(order: 'asc' | 'desc'): void { patchState(store, (state) => ({ filter: { ...state.filter, order }, })); }, loadByQuery: rxMethod( pipe( debounceTime(300), distinctUntilChanged(), tap(() => patchState(store, { isLoading: true })), switchMap((query) => { return booksService.getByQuery(query).pipe( tapResponse({ next: (books) => patchState(store, { books }), error: console.error, finalize: () => patchState(store, { isLoading: false }), }) ); }) ) ), })) ); ``` -------------------------------- ### Initialize Store with `withEntities` Source: https://ngrx.io/guide/signals/signal-store/entity-management Integrates entity state into the store using the `withEntities` feature. Requires entities to have an `id` property. ```typescript import { computed } from '@angular/core'; import { signalStore } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; type Todo = { id: number; text: string; completed: boolean; }; export const TodosStore = signalStore(withEntities()); ``` -------------------------------- ### Combine withEntities and withSelectedEntity Source: https://ngrx.io/guide/signals/signal-store/custom-store-features Demonstrates how to integrate the `withSelectedEntity` feature into a store that already uses `withEntities`. This ensures all necessary state and computed signals are available. ```typescript import { signalStore } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; import { withSelectedEntity } from './with-selected-entity'; import { Book } from './book'; export const BooksStore = signalStore( withEntities(), withSelectedEntity() ); ``` -------------------------------- ### Mocking SignalStore for Component Testing Source: https://ngrx.io/guide/signals/signal-store/testing Demonstrates how to mock a SignalStore for component testing. Two styles are shown: one verifying state change and another verifying method interaction. ```typescript import { Component, inject, signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { patchState, signalStore, withMethods, withState, } from '@ngrx/signals'; import { page } from 'vitest/browser'; const CounterStore = signalStore( { providedIn: 'root' }, withState({ count: 0 }), withMethods((store) => ({ increment() { patchState(store, ({ count }) => ({ count: count + 1 })); }, })) ); @Component({ selector: 'app-counter', template: `

{{ store.count() }}

`, }) class CounterComponent { protected readonly store = inject(CounterStore); } // Test describe('CounterComponent', () => { it('updates displayed count when the mock implements increment', async () => { const count = signal(0); const mockStore = { count, increment() { count.set(count() + 1); }, }; TestBed.configureTestingModule({ providers: [{ provide: CounterStore, useValue: mockStore }], }).createComponent(CounterComponent); await expect .element(page.getByLabelText('count')) .toHaveTextContent('0'); await page.getByRole('button', { name: 'Increment' }).click(); await expect .element(page.getByLabelText('count')) .toHaveTextContent('1'); }); it('calls increment when the button is clicked and mock uses vi.fn()', async () => { const count = signal(0); const increment = vi.fn(() => count.set(count() + 1)); const mockStore = { count, increment }; TestBed.configureTestingModule({ providers: [{ provide: CounterStore, useValue: mockStore }], }).createComponent(CounterComponent); await page.getByRole('button', { name: 'Increment' }).click(); expect(increment).toHaveBeenCalledTimes(1); }); }); ``` -------------------------------- ### Provide SignalStore Globally with 'providedIn: root' Source: https://ngrx.io/guide/signals/signal-store Configures a SignalStore to be provided globally by setting 'providedIn: root' during its definition. This makes the store a singleton accessible throughout the application. ```typescript import { signalStore, withState } from '@ngrx/signals'; import { Book } from './book'; type BookSearchState = { /* ... */ }; const initialState: BookSearchState = { /* ... */ }; export const BookSearchStore = signalStore( // 👇 Providing `BookSearchStore` at the root level. { providedIn: 'root' }, withState(initialState) ); ``` -------------------------------- ### Create Feature with Input Properties and Methods Source: https://ngrx.io/guide/signals/signal-store/custom-store-features Defines a custom feature `withBaz` that requires a specific input property `foo` (a Signal) and an input method `bar`. It also adds its own method `baz` that utilizes these inputs. ```typescript import { Signal } from '@angular/core'; import { signalStoreFeature, type, withMethods } from '@ngrx/signals'; export function withBaz() { return signalStoreFeature( { props: type<{ foo: Signal }>(), methods: type<{ bar(foo: number): void }>(), }, withMethods((store) => ({ baz(): void { const foo = store.foo(); store.bar(typeof foo === 'number' ? foo : Number(foo)); }, })) ); } ``` -------------------------------- ### Define State Transitions with Reducers Source: https://ngrx.io/guide/signals/signal-store/events This snippet demonstrates how to define state transitions in response to events using `withReducer` and `on`. It maps events like `opened`, `queryChanged`, `loadedSuccess`, and `loadedFailure` to specific state updates. ```typescript import { signalStore, withState } from '@ngrx/signals'; import { on, withReducer } from '@ngrx/signals/events'; import { bookSearchEvents } from './book-search-events'; import { booksApiEvents } from './books-api-events'; import { Book } from './book'; type State = { query: string; books: Book[]; isLoading: boolean }; export const BookSearchStore = signalStore( withState({ query: '', books: [], isLoading: false }), withReducer( on(bookSearchEvents.opened, () => ({ isLoading: true })), on(bookSearchEvents.queryChanged, ({ payload: query }) => ({ query, isLoading: true, })), on(booksApiEvents.loadedSuccess, ({ payload: books }) => ({ books, isLoading: false, })), on(booksApiEvents.loadedFailure, () => ({ isLoading: false })) ) ); ``` -------------------------------- ### Fetch Book by ID using rxMethod Source: https://ngrx.io/guide/signals/rxjs-integration Use `rxMethod` to fetch a book by its ID when the `selectedBookId` signal changes. Requires `rxMethod`, `filter`, `concatMap`, and `tapResponse`. Ensure `BooksService` is injected. ```typescript import { Component, inject, signal } from '@angular/core'; import { concatMap, filter, pipe } from 'rxjs'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; import { Book } from './book'; @Component({ /* ... */ }) export class BookList { readonly #booksService = inject(BooksService); readonly bookMap = signal>({}); readonly selectedBookId = signal(null); readonly loadBookById = rxMethod( pipe( filter((id) => !!id && !this.bookMap()[id]), concatMap((id) => { return this.#booksService.getById(id).pipe( tapResponse({ next: (book) => this.addBook(book), error: console.error, }) ); }) ) ); constructor() { // 👇 Load book by id whenever the `selectedBookId` value changes. this.loadBookById(this.selectedBookId); } addBook(book: Book): void { this.bookMap.update((bookMap) => ({ ...bookMap, [book.id]: book, })); } } ``` -------------------------------- ### Provide and Inject SignalStore at Component Level Source: https://ngrx.io/guide/signals/signal-store Demonstrates how to provide a SignalStore locally within a component using the 'providers' array and then inject it into the component class. ```typescript import { Component, inject } from '@angular/core'; import { BookSearchStore } from './book-search-store'; @Component({ /* ... */ // 👇 Providing `BookSearchStore` at the component level. providers: [BookSearchStore], }) export class BookSearch { readonly store = inject(BookSearchStore); } ``` -------------------------------- ### Create a SignalStore with Initial State Source: https://ngrx.io/guide/signals/signal-store Defines a SignalStore named BookSearchStore using the withState feature and an initial state object. This creates signals for each state property. ```typescript import { signalStore, withState } from '@ngrx/signals'; import { Book } from './book'; type BookSearchState = { books: Book[]; isLoading: boolean; filter: { query: string; order: 'asc' | 'desc' }; }; const initialState: BookSearchState = { books: [], isLoading: false, filter: { query: '', order: 'asc' }, }; export const BookSearchStore = signalStore(withState(initialState)); ``` -------------------------------- ### Create a SignalStore with Initial State Factory Source: https://ngrx.io/guide/signals/signal-store Creates a SignalStore using a state factory function that injects initial state from an InjectionToken. This allows for dynamic initial state configuration. ```typescript const BOOK_SEARCH_STATE = new InjectionToken( 'BookSearchState', { factory: () => initialState } ); const BookSearchStore = signalStore( withState(() => inject(BOOK_SEARCH_STATE)) ); ``` -------------------------------- ### SignalState in a Component Source: https://ngrx.io/guide/signals/signal-state Demonstrates how to initialize and use SignalState within an Angular component to manage local state and trigger actions. ```typescript import { ChangeDetectionStrategy, Component, inject, OnInit, } from '@angular/core'; import { BookListStore } from './book-list-store'; @Component({ selector: 'ngrx-book-list', template: `

Books

@if (store.isLoading()) {

Loading...

} @else {
    @for (book of store.books(); track book.id) {
  • {{ book.title }}
  • }
} `, providers: [BookListStore], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BookList { readonly store = inject(BookListStore); constructor() { this.store.loadBooks(); } } ``` -------------------------------- ### Initializing rxMethod Outside Injection Context Source: https://ngrx.io/guide/signals/rxjs-integration Demonstrates how to initialize and use rxMethod outside of a standard Angular injection context by explicitly providing an Injector instance. This is useful for scenarios where components or services are not directly managed by Angular's DI. ```typescript import { Component, inject, Injector, OnInit } from '@angular/core'; import { tap } from 'rxjs'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; @Component({ /* ... */ }) export class Numbers implements OnInit { readonly #injector = inject(Injector); ngOnInit(): void { const logNumber = rxMethod(tap(console.log), { injector: this.#injector, }); logNumber(10); } } ``` -------------------------------- ### Testing a Locally Provided SignalStore Source: https://ngrx.io/guide/signals/signal-store/testing Test a SignalStore that is not provided in root by configuring the testing module to provide it. Asserts the store is defined and its initial state is correct. ```typescript import { TestBed } from '@angular/core/testing'; import { signalStore, withState } from '@ngrx/signals'; const CounterStore = signalStore(withState({ count: 0 })); // Test describe('CounterStore (local)', () => { it('is defined with an initial count', () => { TestBed.configureTestingModule({ // 👇 provide the store in the testing module providers: [CounterStore], }); const store = TestBed.inject(CounterStore); expect(store).toBeDefined(); expect(store.count()).toBe(0); }); }); ``` -------------------------------- ### Testing a Globally Provided SignalStore Source: https://ngrx.io/guide/signals/signal-store/testing Inject and test a SignalStore provided globally with `providedIn: 'root'`. Asserts the store is defined and its initial state is correct. ```typescript import { TestBed } from '@angular/core/testing'; import { signalStore, withState } from '@ngrx/signals'; const CounterStore = signalStore( { providedIn: 'root' }, withState({ count: 0 }) ); // Test describe('CounterStore (global)', () => { it('is defined with an initial count', () => { const store = TestBed.inject(CounterStore); expect(store).toBeDefined(); expect(store.count()).toBe(0); }); }); ``` -------------------------------- ### Set All Entities Source: https://ngrx.io/guide/signals/signal-store/entity-management Replaces the entire current entity collection with a new array of entities. ```typescript patchState(store, setAllEntities([todo1, todo2, todo3])); ``` -------------------------------- ### BookSearch Component Using SignalStore Source: https://ngrx.io/guide/signals/signal-store This component demonstrates how to inject and use the BookSearchStore to manage state, display computed data, and trigger actions like updating the query and loading books. ```typescript import { ChangeDetectionStrategy, Component, inject, } from '@angular/core'; import { BookFilter } from './book-filter'; import { BookList } from './book-list'; import { BooksStore } from './books.store'; @Component({ imports: [BookFilter, BookList], template: `

Books ({{ store.booksCount() }})

`, providers: [BookSearchStore], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BookSearch { readonly store = inject(BookSearchStore); constructor() { const query = this.store.filter.query; // 👇 Re-fetch books whenever the value of query signal changes. this.store.loadByQuery(query); } } ``` -------------------------------- ### Testing a Custom SignalStore Feature Source: https://ngrx.io/guide/signals/signal-store/testing This snippet shows how to test a custom signal store feature. It defines a `withCounter` feature and then creates a minimal `CounterStore` using this feature. The test asserts the initial state, computed values, and the effect of the `increment` method. ```typescript import { TestBed } from '@angular/core/testing'; import { patchState, signalStore, signalStoreFeature, withComputed, withMethods, withState, } from '@ngrx/signals'; function withCounter() { return signalStoreFeature( withState({ count: 0 }), withComputed(({ count }) => ({ doubleCount: () => count() * 2, })), withMethods((store) => ({ increment() { patchState(store, ({ count }) => ({ count: count + 1 })); }, })) ); } // Test describe('withCounter', () => { it('has initial count and doubleCount, and increment updates both', () => { // 👇 "testing store" wraps the feature const CounterStore = signalStore( { providedIn: 'root' }, withCounter() ); const store = TestBed.inject(CounterStore); expect(store.count()).toBe(0); expect(store.doubleCount()).toBe(0); store.increment(); expect(store.count()).toBe(1); expect(store.doubleCount()).toBe(2); }); }); ``` -------------------------------- ### Integrate Request Status Feature into Store Source: https://ngrx.io/guide/signals/signal-store/custom-store-features Adds the `withRequestStatus` feature and its associated state and computed signals to a `BooksStore`. It also includes a `loadAll` method to fetch books and update the store's status. ```typescript import { inject } from '@angular/core'; import { patchState, signalStore, withMethods } from '@ngrx/signals'; import { setAllEntities, withEntities } from '@ngrx/signals/entities'; import { setFulfilled, setPending, withRequestStatus, } from './with-request-status'; import { BooksService } from './books-service'; import { Book } from './book'; export const BooksStore = signalStore( withEntities(), withRequestStatus(), withMethods((store, booksService = inject(BooksService)) => ({ async loadAll() { patchState(store, setPending()); const books = await booksService.getAll(); patchState(store, setAllEntities(books), setFulfilled()); }, })) ); ``` -------------------------------- ### Define SignalStore as a Class Source: https://ngrx.io/guide/signals/faq Demonstrates how to define a SignalStore using a class-based approach by extending `signalStore`. This approach is an alternative to the recommended functional style. ```typescript @Injectable() export class CounterStore extends signalStore( { protectedState: false }, withState({ count: 0 }) ) { readonly doubleCount = computed(() => this.count() * 2); increment(): void { patchState(this, { count: this.count() + 1 }); } } ``` -------------------------------- ### Synchronous State Tracking with watchState Source: https://ngrx.io/guide/signals/signal-store/state-tracking Utilize `watchState` for synchronous tracking of every SignalStore state change. It logs each state update individually, unlike `effect` which coalesces updates. Requires an injection context by default. ```typescript import { effect } from '@angular/core'; import { getState, patchState, signalStore, watchState, withHooks, withState, } from '@ngrx/signals'; export const CounterStore = signalStore( withState({ count: 0 }), withMethods((store) => ({ increment(): void { patchState(store, { count: store.count() + 1 }); }, })), withHooks({ onInit(store) { watchState(store, (state) => { console.log('[watchState] counter state', state); }); // logs: { count: 0 }, { count: 1 }, { count: 2 } effect(() => { console.log('[effect] counter state', getState(store)); }); // logs: { count: 2 } store.increment(); store.increment(); }, }) ); ``` -------------------------------- ### Store with Entity Management Methods Source: https://ngrx.io/guide/signals/signal-store/entity-management Combines `withEntities` with `withMethods` to add custom methods for entity manipulation, such as adding, removing, or updating todos. ```typescript import { patchState, signalStore, withMethods } from '@ngrx/signals'; import { addEntity, removeEntities, updateAllEntities, withEntities, } from '@ngrx/signals/entities'; type Todo = { /* ... */ }; export const TodosStore = signalStore( withEntities(), withMethods((store) => ({ addTodo(todo: Todo): void { patchState(store, addEntity(todo)); }, removeEmptyTodos(): void { patchState( store, removeEntities(({ text }) => !text) ); }, completeAllTodos(): void { patchState(store, updateAllEntities({ completed: true })); }, })) ); ``` -------------------------------- ### Define Methods in Signal Store Source: https://ngrx.io/guide/signals/signal-store Use `withMethods` to add custom logic and state manipulation functions to the store. The factory function receives the store instance and can access its state, properties, and other methods. ```typescript import { computed } from '@angular/core'; import { patchState, signalStore, withComputed, withMethods, withState, } from '@ngrx/signals'; import { Book } from './book'; type BookSearchState = { /* ... */ }; const initialState: BookSearchState = { /* ... */ }; export const BookSearchStore = signalStore( withState(initialState), withComputed(/* ... */), // 👇 Accessing a store instance with previously defined state signals, // properties, and methods. withMethods((store) => ({ updateQuery(query: string): void { // 👇 Updating state using the `patchState` function. patchState(store, (state) => ({ filter: { ...state.filter, query }, })); }, updateOrder(order: 'asc' | 'desc'): void { patchState(store, (state) => ({ filter: { ...state.filter, order }, })); }, })) ); ``` -------------------------------- ### Mocking Service Dependencies in Signal Stores Source: https://ngrx.io/guide/signals/signal-store/testing Mock services injected into a Signal Store by providing a mock implementation via `useValue` in `TestBed.configureTestingModule`. This ensures predictable behavior for assertions. ```typescript import { TestBed } from '@angular/core/testing'; import { patchState, signalStore, withMethods, withState, } from '@ngrx/signals'; import { inject, Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) class StepService { getStep() { return 1; } } const CounterStore = signalStore( { providedIn: 'root' }, withState({ count: 0 }), withMethods((store, stepService = inject(StepService)) => ({ increment() { patchState(store, ({ count }) => ({ count: count + stepService.getStep(), })); }, })) ); // Test describe('CounterStore with StepService', () => { it('increments by the step returned by the injected service', () => { const mockStepService = { getStep: () => 3 }; TestBed.configureTestingModule({ providers: [ // 👇 provide the mock service { provide: StepService, useValue: mockStepService }, ], }); const store = TestBed.inject(CounterStore); store.increment(); expect(store.count()).toBe(3); }); }); ``` -------------------------------- ### Manual cleanup for signalMethod from service Source: https://ngrx.io/guide/signals/signal-method Demonstrates how to manually provide the caller's injector to signalMethod when called from a service to ensure cleanup on component destroy. ```typescript @Component({ /* ... */ }) export class Numbers implements OnInit { readonly numbersService = inject(NumbersService); readonly injector = inject(Injector); ngOnInit(): void { const value = signal(1); // 👇 Providing the `Numbers` component injector // to ensure cleanup on component destroy. this.numbersService.logDoubledNumber(value, { injector: this.injector, }); // 👇 No need to provide an injector for static values. this.numbersService.logDoubledNumber(2); } } ``` -------------------------------- ### Define Custom State Updaters Source: https://ngrx.io/guide/signals/signal-state Create reusable custom state updaters using `PartialStateUpdater`. These updaters can be easily tested and reused. ```typescript import { PartialStateUpdater } from '@ngrx/signals'; function setFirstName( firstName: string ): PartialStateUpdater<{ user: User }> { return (state) => ({ user: { ...state.user, firstName } }); } const setAdmin = () => ({ isAdmin: true }); ``` -------------------------------- ### Prepend Multiple Entities Source: https://ngrx.io/guide/signals/signal-store/entity-management Adds multiple entities to the beginning of the collection, preserving their relative order. Entities with existing IDs are not added. ```typescript patchState(store, prependEntities([todo1, todo2])); ``` -------------------------------- ### Inject SignalStore via Constructor Source: https://ngrx.io/guide/signals/faq Illustrates how to inject a SignalStore into a component's constructor. Ensure the SignalStore type is exported with the same name for successful injection. ```typescript // counter-store.ts export const CounterStore = signalStore(withState({ count: 0 })); export type CounterStore = InstanceType; // counter.ts import { CounterStore } from './counter.store'; @Component({ /* ... */ }) export class Counter { constructor(readonly store: CounterStore) {} } ``` -------------------------------- ### Create SignalState with Initial State Source: https://ngrx.io/guide/signals/signal-state Instantiate SignalState using the signalState function with an initial state object. The state's type must be a record or object literal. ```typescript import { signalState } from '@ngrx/signals'; import { User } from './user'; type UserState = { user: User; isAdmin: boolean }; const userState = signalState({ user: { firstName: 'Eric', lastName: 'Clapton' }, isAdmin: false, }); ``` -------------------------------- ### Lifecycle Hooks with Factory Function and Dependencies Source: https://ngrx.io/guide/signals/signal-store/lifecycle-hooks Utilize a factory function with `withHooks` to inject dependencies like `Logger` and manage resources like intervals, ensuring cleanup in `onDestroy`. ```typescript export const CounterStore = signalStore( /* ... */ withHooks((store) => { const logger = inject(Logger); let interval = 0; return { onInit() { interval = setInterval(() => store.increment(), 2_000); }, onDestroy() { logger.info('count on destroy', store.count()); clearInterval(interval); }, }; }) ); ``` -------------------------------- ### Operations on Named Entity Collections Source: https://ngrx.io/guide/signals/signal-store/entity-management Demonstrates how to perform operations like adding and removing entities within a named collection, requiring the collection name in the updater configuration. ```typescript import { patchState, signalStore, type, withMethods, } from '@ngrx/signals'; import { addEntity, removeEntity, withEntities, } from '@ngrx/signals/entities'; type Todo = { /* ... */ }; export const TodosStore = signalStore( withEntities({ entity: type(), collection: 'todo' }), withMethods((store) => ({ addTodo(todo: Todo): void { patchState(store, addEntity(todo, { collection: 'todo' })); }, removeTodo(id: number): void { patchState(store, removeEntity(id, { collection: 'todo' })); }, })) ); ``` -------------------------------- ### Managing Multiple Named Entity Collections Source: https://ngrx.io/guide/signals/signal-store/entity-management Illustrates how to manage multiple distinct entity collections (e.g., book, author, category) within a single NgRx Signals store by using `withEntities` multiple times. ```typescript export const LibraryStore = signalStore( withEntities({ entity: type(), collection: 'book' }), withEntities({ entity: type(), collection: 'author' }), withEntities({ entity: type(), collection: 'category' }), withMethods((store) => ({ addBook(book: Book): void { patchState(store, addEntity(book, { collection: 'book' })); }, addAuthor(author: Author): void { patchState(store, addEntity(author, { collection: 'author' })); }, addCategory(category: Category): void { patchState( store, addEntity(category, { collection: 'category' }) ); }, })) ); ``` -------------------------------- ### SignalState in a Service Source: https://ngrx.io/guide/signals/signal-state Illustrates creating a SignalState within an Angular service to manage application state, including loading and data fetching logic. ```typescript import { inject, Injectable } from '@angular/core'; import { exhaustMap, pipe, tap } from 'rxjs'; import { signalState, patchState } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BooksService } from './books-service'; import { Book } from './book'; type BookListState = { books: Book[]; isLoading: boolean }; const initialState: BookListState = { books: [], isLoading: false, }; @Injectable() export class BookListStore { readonly #booksService = inject(BooksService); readonly #state = signalState(initialState); readonly books = this.#state.books; readonly isLoading = this.#state.isLoading; readonly loadBooks = rxMethod( pipe( tap(() => patchState(this.#state, { isLoading: true })), exhaustMap(() => { return this.#booksService.getAll().pipe( tapResponse({ next: (books) => patchState(this.#state, { books }), error: console.error, finalize: () => patchState(this.#state, { isLoading: false }), }) ); }) ) ); } ```