Skip to content

Commit

Permalink
docs(signals): add updated docs for @ngrx/signals (#4165)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonroberts authored Dec 8, 2023
1 parent a726cfb commit 0315124
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 3 deletions.
92 changes: 91 additions & 1 deletion projects/ngrx.io/content/guide/signals/rxjs-integration.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,93 @@
# RxJS Integration

**UNDER CONSTRUCTION**
RxJS is still a major part of NgRx and the Angular ecosystem, and the NgRx Signals package provides **opt-in** usage to interact with RxJS observables using the `rxMethod` function.

The `rxMethod` function allows you to define a method that can receive a signal or observable, read its latest values, and perform additional operations with an observable.

<code-example header="users.store.ts">
import { inject } from '@angular/core';
import { debounceTime, distinctUntilChanged, pipe, switchMap, tap } from 'rxjs';
import {
signalStore,
patchState,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';

import { User } from './user.model';
import { UsersService } from './users.service';

type State = { users: User[]; isLoading: boolean; query: string };

const initialState: State = {
users: [],
isLoading: false,
query: '',
};

export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods((store, usersService = inject(UsersService)) =&gt; ({
updateQuery(query: string) {
patchState(store, { query });
},
loadByQuery: rxMethod&lt;;string&gt;(
pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() =&gt; patchState(store, { isLoading: true })),
switchMap((query) =&gt;
usersService.getByQuery(query).pipe(
tapResponse({
next: (users) =&gt; patchState(store, { users }),
error: console.error,
finalize: () =&gt; patchState(store, { isLoading: false }),
}),
),
),
),
),
})),
withHooks({
onInit({ loadByQuery, query }) {
loadByQuery(query);
},
}),
);
</code-example>

The example `UserStore` above uses the `rxMethod` operator to create a method that loads the users on initialization of the store based on a query string.

The `UsersStore` can then be used in the component, along with its additional methods, providing a clean, structured way to manage state with signals, combined with the power of RxJS observable streams for asynchronous behavior.

<code-example header="users.component.ts">
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { SearchBoxComponent } from './ui/search-box.component';
import { UserListComponent } from './ui/user-list.component';
import { UsersStore } from './users.store';

@Component({
selector: 'app-users',
standalone: true,
imports: [SearchBoxComponent, UserListComponent],
template: `
&lt;h1&gt;Users (RxJS Integration)&lt;/h1&gt;

&lt;app-search-box
[query]="store.query()"
(queryChange)="store.updateQuery($event)"
/&gt;

&lt;app-user-list [users]="store.users()" [isLoading]="store.isLoading()" /&gt;
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class UsersComponent {
readonly store = inject(UsersStore);
}
</code-example>
42 changes: 41 additions & 1 deletion projects/ngrx.io/content/guide/signals/signal-state.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
# SignalState

**UNDER CONSTRUCTION**
Not every piece of state needs its own store. For this use case, `@ngrx/signals` comes with a `signalState` utility.

The `signalState` function is used:

- To create and operate on small slices of state.
- Directly in your component class, service, or a standalone function.
- Provide a deeply nested signal of the object properties.

<code-example header="counter.component.ts">
import { Component } from '@angular/core';
import { signalState, patchState } from '@ngrx/signals';

@Component({
selector: 'app-counter',
standalone: true,
template: `
Count: {{ state.count() }}

&lt;button (click)="increment()"&gt;Increment&lt;/button&gt;
&lt;button (click)="decrement()"&gt;Decrement&lt;/button&gt;
&lt;button (click)="reset()"&gt;Reset&lt;/button&gt;
`,
})
export class CounterComponent {
state = signalState({ count: 0 });

increment() {
patchState(this.state, (state) => ({ count: state.count + 1 }));
}

decrement() {
patchState(this.state, (state) => ({ count: state.count - 1 }));
}

reset() {
patchState(this.state, { count: 0 });
}
}
</code-example>

The `patchState` utility function provides a type-safe way to perform immutable updates on pieces of state.
136 changes: 135 additions & 1 deletion projects/ngrx.io/content/guide/signals/signal-store/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,137 @@
# SignalStore

**UNDER CONSTRUCTION**
For managing larger stores with more complex pieces of state, you can use the `signalStore` utility function, along with patchState, and other functions to manage the state.

## Creating a Store

To create a signal store, use the `signalStore` function and the `withState` function:

<code-example header="counter.store.ts">
import { signalStore, withState } from '@ngrx/signals';

export const CounterStore = signalStore(
withState({ count: 0 })
);
</code-example>

The `withState` function takes the initial state of the store and defines the shape of the state.

<div class="callout is-critical">

This store is not registered with _any_ injectors, and must be provided in a `providers` array at the component, route, or root level before injected.

</div>

## Defining Computed Values

Computed properties can also be derived from existing pieces of state in the store using the `withComputed` function.

<code-example header="counter.store.ts">
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed } from '@ngrx/signals';

export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
);
</code-example>

The `doubleCount` computed signal reacts to changes to the `count` signal.

## Defining Store Methods

You can also define methods that are exposed publicly to operate on the store with a well-defined API.

<code-example header="counter.store.ts">
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed, withMethods } from '@ngrx/signals';

export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
decrement() {
patchState(store, { count: count() - 1 });
},
}))
);
</code-example>

## Providing and Injecting the Store

Stores can be used locally and globally.

### Providing a Component-Level SignalStore

To provide a store and tie it to a component's lifecycle, add it to the `providers` array of your component, and inject it using dependency injection.

<code-example header="counter.component.ts">
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { CounterStore } from './counter.store';

@Component({
selector: 'app-counter',
standalone: true,
template: `
&lt;h1&gt;Counter (signalStore)&lt;/h1&gt;

&lt;p&gt;Count: {{ store.count() }}&lt;/p&gt;
&lt;p&gt;Double Count: {{ store.doubleCount() }}&lt;/p&gt;

&lt;button (click)="store.increment()"&gt;Increment&lt;/button&gt;
&lt;button (click)="store.decrement()"&gt;Decrement&lt;/button&gt;
`,
providers: [CounterStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CounterComponent {
readonly store = inject(CounterStore);
}
</code-example>

### Providing a Global-Level SignalStore

You can also define a signal store to be used a global level. When defining the signal store, use the `providedIn` syntax:

<code-example header="counter.component.ts">
import { signalStore, withState } from '@ngrx/signals';

export const CounterStore = signalStore(
{ providedIn: 'root' },
withState({ count: 0 })
);
</code-example>

Now the store can be used globally across the application using a singleton instance.

<code-example header="counter.store.ts" linenumbers="false">
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { CounterStore } from './counter.store';

@Component({
selector: 'app-counter',
standalone: true,
template: `
&lt;h1&gt;Counter (signalStore)&lt;/h1&gt;

&lt;p&gt;Count: {{ store.count() }}&lt;/p&gt;
&lt;p&gt;Double Count: {{ store.doubleCount() }}&lt;/p&gt;

&lt;button (click)="store.increment()"&gt;Increment&lt;/button&gt;
&lt;button (click)="store.decrement()"&gt;Decrement&lt;/button&gt;
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CounterComponent {
readonly store = inject(CounterStore);
}
</code-example>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Lifecycle Hooks

You can also create lifecycle hooks that are called when the store is created or destroyed.
Lifecycle hooks can be used to initialize fetching data, updating state, and more.

<code-example header="counter.store.ts">
import { computed } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import {
signalStore,
withState,
patchState,
withComputed,
withHooks,
withMethods,
} from '@ngrx/signals';

export const CounterStore = signalStore(
withState({ count: 0 }),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
})),
withHooks({
onInit({ increment }) {
interval(2_000)
.pipe(takeUntilDestroyed())
.subscribe(() => increment());
},
onDestroy({ count }) {
console.log('count on destroy', count());
},
}),
);
</code-example>

In the example above, the `onInit` hook subscribes to an interval observable, and calls the `increment` method on the store to increment the count every 2 seconds. The lifecycle methods also have access to the injection context for automatic cleanup using the `takeUntilDestroyed()` function from Angular.
4 changes: 4 additions & 0 deletions projects/ngrx.io/content/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@
"title": "Core Concepts",
"url": "guide/signals/signal-store"
},
{
"title": "Lifecycle Hooks",
"url": "guide/signals/signal-store/lifecycle-hooks"
},
{
"title": "Custom Store Features",
"url": "guide/signals/signal-store/custom-store-features"
Expand Down

0 comments on commit 0315124

Please sign in to comment.