Angular course ยท Module 10: State Management
SignalStore (NGRX 17+)
NgRx SignalStore is a modern, signal-based library for state management.
Installation and Basics
npm install @ngrx/signalsimport { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
interface SamuraiState {
samurai: Samurai[];
loading: boolean;
filter: string;
}
const initialState: SamuraiState = {
samurai: [],
loading: false,
filter: ''
};
export const SamuraiStore = signalStore(
{ providedIn: 'root' },
// State
withState(initialState),
// Computed signals
withComputed((store) => ({
filteredSamurai: computed(() => {
const filter = store.filter().toLowerCase();
return store.samurai().filter(s =>
s.name.toLowerCase().includes(filter)
);
}),
count: computed(() => store.samurai().length)
})),
// Methods (actions)
withMethods((store, http = inject(HttpClient)) => ({
loadSamurai(): void {
patchState(store, { loading: true });
http.get<Samurai[]>('/api/samurai').subscribe({
next: (samurai) => patchState(store, { samurai, loading: false }),
error: () => patchState(store, { loading: false })
});
},
setFilter(filter: string): void {
patchState(store, { filter });
},
addSamurai(samurai: Samurai): void {
patchState(store, (state) => ({
samurai: [...state.samurai, samurai]
}));
}
}))
);
// Usage in a component
@Component({
template: `
<input (input)="store.setFilter($any($event.target).value)">
@for (s of store.filteredSamurai(); track s.id) {
<div>{{ s.name }}</div>
}
`
})
export class SamuraiListComponent {
store = inject(SamuraiStore);
constructor() {
this.store.loadSamurai();
}
}Code for this lesson: src/app/app.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<div class="ngrx-guide">
<h1>NgRx SignalStore - Guide</h1>
<section>
<h2>What is NgRx SignalStore?</h2>
<p>NgRx SignalStore is a modern state management library that uses Angular Signals. It is simpler than the classic NgRx Store.</p>
<div class="comparison">
<div class="box classic">
<h3>Classic NgRx</h3>
<ul>
<li>Actions</li>
<li>Reducers</li>
<li>Effects</li>
<li>Selectors</li>
<li>Lots of boilerplate</li>
</ul>
</div>
<div class="box signal">
<h3>NgRx SignalStore</h3>
<ul>
<li>withState()</li>
<li>withComputed()</li>
<li>withMethods()</li>
<li>Less code</li>
<li>Signals!</li>
</ul>
</div>
</div>
</section>
<section>
<h2>Creating a SignalStore</h2>
<pre><code>{{ storeCode }}</code></pre>
</section>
<section>
<h2>Using in a component</h2>
<pre><code>{{ componentCode }}</code></pre>
</section>
<section>
<h2>Asynchronous effects</h2>
<pre><code>{{ effectsCode }}</code></pre>
</section>
<section>
<h2>Configuration</h2>
<pre><code>{{ configCode }}</code></pre>
</section>
<div class="tips">
<h2>Tips</h2>
<ul>
<li><strong>When to use SignalStore?</strong> When state is shared between many components.</li>
<li><strong>When are Signals enough?</strong> For local component state.</li>
<li><strong>Immutability:</strong> Always create new objects instead of mutating existing ones.</li>
<li><strong>Selectors:</strong> Use withComputed() for derived data.</li>
</ul>
</div>
</div>
`,
styles: [`
.ngrx-guide {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
color: #eee;
}
h1 {
text-align: center;
color: #64ffda;
}
h2 {
color: #64ffda;
border-bottom: 2px solid #64ffda;
padding-bottom: 10px;
}
section {
background: rgba(0,0,0,0.3);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
pre {
background: #0d1421;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
border-left: 4px solid #64ffda;
}
code {
color: #e6e6e6;
font-family: 'Fira Code', monospace;
font-size: 0.85em;
line-height: 1.5;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
@media (max-width: 600px) {
.comparison {
grid-template-columns: 1fr;
}
}
.box {
padding: 15px;
border-radius: 10px;
}
.box.classic {
background: rgba(255, 107, 107, 0.2);
border: 2px solid #ff6b6b;
}
.box.signal {
background: rgba(100, 255, 218, 0.2);
border: 2px solid #64ffda;
}
.box h3 {
margin-top: 0;
}
.box ul {
margin: 0;
padding-left: 20px;
}
.box li {
margin: 5px 0;
}
.tips {
background: rgba(100, 255, 218, 0.1);
border: 2px solid #64ffda;
border-radius: 10px;
padding: 20px;
}
.tips ul {
list-style: none;
padding: 0;
}
.tips li {
padding: 10px 0;
border-bottom: 1px solid rgba(100, 255, 218, 0.2);
}
.tips li:last-child {
border-bottom: none;
}
`]
})
export class AppComponent {
storeCode = `// samurai.store.ts
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';
interface SamuraiState {
samuraiList: Samurai[];
selectedId: number | null;
loading: boolean;
}
const initialState: SamuraiState = {
samuraiList: [],
selectedId: null,
loading: false
};
export const SamuraiStore = signalStore(
// Initial state
withState(initialState),
// Selectors (computed)
withComputed((store) => ({
selectedSamurai: computed(() => {
const id = store.selectedId();
return store.samuraiList().find(s => s.id === id) ?? null;
}),
totalCount: computed(() => store.samuraiList().length),
totalGold: computed(() =>
store.samuraiList().reduce((sum, s) => sum + s.gold, 0)
)
})),
// Methods (actions)
withMethods((store) => ({
selectSamurai(id: number): void {
patchState(store, { selectedId: id });
},
addSamurai(samurai: Samurai): void {
patchState(store, {
samuraiList: [...store.samuraiList(), samurai]
});
},
removeSamurai(id: number): void {
patchState(store, {
samuraiList: store.samuraiList().filter(s => s.id !== id),
selectedId: store.selectedId() === id ? null : store.selectedId()
});
}
}))
);`;
componentCode = `// samurai-list.component.ts
import { Component, inject } from '@angular/core';
import { SamuraiStore } from './samurai.store';
@Component({
selector: 'app-samurai-list',
standalone: true,
providers: [SamuraiStore], // Or in app.config.ts for global state
template: \`
<div>
<p>Number of samurai: {{ store.totalCount() }}</p>
<p>Total gold: {{ store.totalGold() }}</p>
@for (samurai of store.samuraiList(); track samurai.id) {
<div (click)="store.selectSamurai(samurai.id)">
{{ samurai.name }} - {{ samurai.gold }} gold
</div>
}
@if (store.selectedSamurai(); as selected) {
<div class="detail">
Selected: {{ selected.name }}
</div>
}
</div>
\`
})
export class SamuraiListComponent {
store = inject(SamuraiStore);
}`;
effectsCode = `// samurai.store.ts - with HTTP effects
import { signalStore, withState, withMethods } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { pipe, switchMap, tap } from 'rxjs';
export const SamuraiStore = signalStore(
withState(initialState),
withMethods((store, http = inject(HttpClient)) => ({
// Method with RxJS
loadSamurai: rxMethod<void>(
pipe(
tap(() => patchState(store, { loading: true })),
switchMap(() => http.get<Samurai[]>('/api/samurai')),
tap(samuraiList => patchState(store, {
samuraiList,
loading: false
}))
)
),
// async/await method
async loadSamuraiAsync(): Promise<void> {
patchState(store, { loading: true });
const response = await fetch('/api/samurai');
const samuraiList = await response.json();
patchState(store, { samuraiList, loading: false });
}
}))
);`;
configCode = `// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { SamuraiStore } from './stores/samurai.store';
export const appConfig: ApplicationConfig = {
providers: [
// Global store - available throughout the app
SamuraiStore,
// Or with feature flags
provideStore(),
provideState({ name: 'samurai', reducer: samuraiReducer }),
provideEffects(SamuraiEffects)
]
};`;
}Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the patchState() function used for in NgRx SignalStore?
2. What does the withState() function define in signalStore()?
Hands-on tasks in the game
- Horizontal ordering
Arrange the signalStore call with its main blocks in their typical written order (one line, left to right):
- Code editor
Complete the SignalStore for managing samurai.
- Horizontal ordering
Arrange the command to install the NgRx SignalStore library (one line, left to right):
- Click in order
Click the elements in order to build a field that injects a SignalStore in a component (modern Angular):