Kurs Angular · Moduł 10: Zarządzanie stanem
SignalStore (NGRX 17+)
NgRx SignalStore to nowoczesna, signal-based biblioteka do zarządzania stanem.
Instalacja i Podstawy
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]
}));
}
}))
);
// Użycie w komponencie
@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();
}
}Kod do tej lekcji: 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 - Przewodnik</h1>
<section>
<h2>Czym jest NgRx SignalStore?</h2>
<p>NgRx SignalStore to nowoczesna biblioteka do zarządzania stanem, która wykorzystuje Angular Signals. Jest prostsza niż klasyczny NgRx Store.</p>
<div class="comparison">
<div class="box classic">
<h3>Klasyczny NgRx</h3>
<ul>
<li>Actions</li>
<li>Reducers</li>
<li>Effects</li>
<li>Selectors</li>
<li>Dużo boilerplate</li>
</ul>
</div>
<div class="box signal">
<h3>NgRx SignalStore</h3>
<ul>
<li>withState()</li>
<li>withComputed()</li>
<li>withMethods()</li>
<li>Mniej kodu</li>
<li>Signals!</li>
</ul>
</div>
</div>
</section>
<section>
<h2>Tworzenie SignalStore</h2>
<pre><code>{{ storeCode }}</code></pre>
</section>
<section>
<h2>Używanie w komponencie</h2>
<pre><code>{{ componentCode }}</code></pre>
</section>
<section>
<h2>Efekty asynchroniczne</h2>
<pre><code>{{ effectsCode }}</code></pre>
</section>
<section>
<h2>Konfiguracja</h2>
<pre><code>{{ configCode }}</code></pre>
</section>
<div class="tips">
<h2>Wskazówki</h2>
<ul>
<li><strong>Kiedy używać SignalStore?</strong> Gdy stan jest współdzielony między wieloma komponentami.</li>
<li><strong>Kiedy wystarczą Signals?</strong> Dla lokalnego stanu komponentu.</li>
<li><strong>Immutability:</strong> Zawsze twórz nowe obiekty zamiast mutować istniejące.</li>
<li><strong>Selektory:</strong> Używaj withComputed() dla pochodnych danych.</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(
// Stan początkowy
withState(initialState),
// Selektory (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)
)
})),
// Metody (akcje)
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], // Lub w app.config.ts dla globalnego stanu
template: \`
<div>
<p>Liczba samurajów: {{ store.totalCount() }}</p>
<p>Suma złota: {{ store.totalGold() }}</p>
@for (samurai of store.samuraiList(); track samurai.id) {
<div (click)="store.selectSamurai(samurai.id)">
{{ samurai.name }} - {{ samurai.gold }} złota
</div>
}
@if (store.selectedSamurai(); as selected) {
<div class="detail">
Wybrany: {{ selected.name }}
</div>
}
</div>
\`
})
export class SamuraiListComponent {
store = inject(SamuraiStore);
}`;
effectsCode = `// samurai.store.ts - z efektami HTTP
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)) => ({
// Metoda z RxJS
loadSamurai: rxMethod<void>(
pipe(
tap(() => patchState(store, { loading: true })),
switchMap(() => http.get<Samurai[]>('/api/samurai')),
tap(samuraiList => patchState(store, {
samuraiList,
loading: false
}))
)
),
// Metoda async/await
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: [
// Globalny store - dostępny w całej aplikacji
SamuraiStore,
// Lub z feature flags
provideStore(),
provideState({ name: 'samurai', reducer: samuraiReducer }),
provideEffects(SamuraiEffects)
]
};`;
}Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy funkcja patchState() w NgRx SignalStore?
2. Co definiuje funkcja withState() w signalStore()?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż wywołanie signalStore z jego głównymi blokami w typowej kolejności zapisu (jedna linia, od lewej do prawej):
- Edytor kodu
Uzupełnij SignalStore dla zarządzania samurajami.
- Układanie w poziomie
Ułóż komendę instalacji biblioteki NgRx SignalStore (jedna linia, od lewej do prawej):
- Klikanie w kolejności
Kliknij elementy w kolejności, aby zbudować pole wstrzykujące SignalStore w komponencie (modern Angular):