-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(signals): add updated docs for @ngrx/signals (#4165)
- Loading branch information
1 parent
a726cfb
commit 0315124
Showing
5 changed files
with
310 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) => ({ | ||
updateQuery(query: string) { | ||
patchState(store, { query }); | ||
}, | ||
loadByQuery: rxMethod<;string>( | ||
pipe( | ||
debounceTime(300), | ||
distinctUntilChanged(), | ||
tap(() => patchState(store, { isLoading: true })), | ||
switchMap((query) => | ||
usersService.getByQuery(query).pipe( | ||
tapResponse({ | ||
next: (users) => patchState(store, { users }), | ||
error: console.error, | ||
finalize: () => 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: ` | ||
<h1>Users (RxJS Integration)</h1> | ||
|
||
<app-search-box | ||
[query]="store.query()" | ||
(queryChange)="store.updateQuery($event)" | ||
/> | ||
|
||
<app-user-list [users]="store.users()" [isLoading]="store.isLoading()" /> | ||
`, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export default class UsersComponent { | ||
readonly store = inject(UsersStore); | ||
} | ||
</code-example> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() }} | ||
|
||
<button (click)="increment()">Increment</button> | ||
<button (click)="decrement()">Decrement</button> | ||
<button (click)="reset()">Reset</button> | ||
`, | ||
}) | ||
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
136
projects/ngrx.io/content/guide/signals/signal-store/index.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: ` | ||
<h1>Counter (signalStore)</h1> | ||
|
||
<p>Count: {{ store.count() }}</p> | ||
<p>Double Count: {{ store.doubleCount() }}</p> | ||
|
||
<button (click)="store.increment()">Increment</button> | ||
<button (click)="store.decrement()">Decrement</button> | ||
`, | ||
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: ` | ||
<h1>Counter (signalStore)</h1> | ||
|
||
<p>Count: {{ store.count() }}</p> | ||
<p>Double Count: {{ store.doubleCount() }}</p> | ||
|
||
<button (click)="store.increment()">Increment</button> | ||
<button (click)="store.decrement()">Decrement</button> | ||
`, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export default class CounterComponent { | ||
readonly store = inject(CounterStore); | ||
} | ||
</code-example> |
39 changes: 39 additions & 0 deletions
39
projects/ngrx.io/content/guide/signals/signal-store/lifecycle-hooks.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters