NgRx Signal Store: Základy a Provider Strategies (Časť 1)
6 min
NgRx Signal Store: Modern state management za 10 minút
"Potrebujem state management. Reactive. Type-safe. Bez boilerplate." 🔥
NgRx Signal Store je odpoveď.
Prečo NgRx Signal Store?
typescript
// BEFORE (classic NgRx)
interface State { count: number; }
const initialState: State = { count: 0 };
const reducer = createReducer(...)
const actions = createAction(...)
const selectors = createSelector(...)
// 100+ lines of boilerplate 😰
// AFTER (Signal Store)
export const CounterStore = signalStore(
withState({ count: 0 }),
withMethods(store => ({
increment: () => store.count.update(c => c + 1)
}))
);
// 5 lines. Done. 🎉NgRx Signal Store features:
- ✅ Zero boilerplate
- ✅ Type-safe (full inference)
- ✅ Reactive (signals)
- ✅ Composable (features)
- ✅ Official NgRx solution
Poďme na to!
Kapitola 1: Quick Start
Krok 1: Installation (1 minúta)
bash
# Install NgRx Signals
npm install @ngrx/signals
# That's it! No other dependencies neededKrok 2: Simple Counter Store (2 minúty)
typescript
// stores/counter.store.ts
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';
export const CounterStore = signalStore(
// 1. Define state
withState({ count: 0 }),
// 2. Add computed values
withComputed(({ count }) => ({
doubled: computed(() => count() * 2),
isEven: computed(() => count() % 2 === 0)
})),
// 3. Add methods
withMethods((store) => ({
increment() {
patchState(store, { count: store.count() + 1 });
},
decrement() {
patchState(store, { count: store.count() - 1 });
},
reset() {
patchState(store, { count: 0 });
}
}))
);Usage v komponente:
typescript
// counter.component.ts
import { Component } from '@angular/core';
import { CounterStore } from './stores/counter.store';
@Component({
selector: 'app-counter',
standalone: true,
providers: [CounterStore], // Provide store
template: `
<div>
<h3>Count: {{ store.count() }}</h3>
<p>Doubled: {{ store.doubled() }}</p>
<p>Is Even: {{ store.isEven() }}</p>
<button (click)="store.increment()">+</button>
<button (click)="store.decrement()">-</button>
<button (click)="store.reset()">Reset</button>
</div>
`
})
export class CounterComponent {
readonly store = inject(CounterStore);
}Total time: 3 minúty!
Kapitola 2: Provider Strategies
Strategy 1: providedIn: 'root' (Singleton)
typescript
// stores/user.store.ts
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';
interface User {
id: string;
name: string;
email: string;
}
interface UserState {
user: User | null;
loading: boolean;
}
export const UserStore = signalStore(
{ providedIn: 'root' }, // ← SINGLETON!
withState<UserState>({
user: null,
loading: false
}),
withComputed(({ user }) => ({
isAuthenticated: computed(() => user() !== null),
userName: computed(() => user()?.name ?? 'Guest')
})),
withMethods((store) => ({
setUser(user: User) {
patchState(store, { user });
},
logout() {
patchState(store, { user: null });
},
setLoading(loading: boolean) {
patchState(store, { loading });
}
}))
);Charakteristiky:
typescript
scope: "Celá aplikácia"
lifecycle: "App bootstrap → app destroy"
sharing: "Všetky komponenty zdieľajú ROVNAKÝ store"
// Usage:
class ComponentA {
store = inject(UserStore); // Gets THE SAME instance
}
class ComponentB {
store = inject(UserStore); // Gets THE SAME instance as A
}Kedy použiť:
typescript
✅ User authentication state
✅ Global app settings
✅ Theme/language preferences
✅ Shopping cart (single cart per user)
✅ Notifications
✅ Any state shared ACROSS the entire app
❌ Form state (každý form má vlastný)
❌ Component-specific state
❌ Temporary UI stateStrategy 2: Provided in Component
typescript
// stores/todo-form.store.ts
import { signalStore, withState, withMethods } from '@ngrx/signals';
interface TodoForm {
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
}
export const TodoFormStore = signalStore(
// NO providedIn! Will be provided in component
withState<TodoForm>({
title: '',
description: '',
priority: 'medium'
}),
withMethods((store) => ({
updateTitle(title: string) {
patchState(store, { title });
},
updateDescription(description: string) {
patchState(store, { description });
},
updatePriority(priority: TodoForm['priority']) {
patchState(store, { priority });
},
reset() {
patchState(store, {
title: '',
description: '',
priority: 'medium'
});
}
}))
);
// todo-form.component.ts
@Component({
selector: 'app-todo-form',
standalone: true,
providers: [TodoFormStore], // ← Provided HERE!
template: `
<form>
<input
[value]="store.title()"
(input)="store.updateTitle($any($event.target).value)"
/>
<textarea
[value]="store.description()"
(input)="store.updateDescription($any($event.target).value)"
></textarea>
<select
[value]="store.priority()"
(change)="store.updatePriority($any($event.target).value)"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<button type="button" (click)="store.reset()">Reset</button>
</form>
`
})
export class TodoFormComponent {
readonly store = inject(TodoFormStore);
}Charakteristiky:
typescript
scope: "Iba táto komponenta + jej children"
lifecycle: "Component create → component destroy"
sharing: "Každá inštancia komponenty má VLASTNÝ store"
// Príklad:
<app-todo-form /> // Own store instance #1
<app-todo-form /> // Own store instance #2
<app-todo-form /> // Own store instance #3
// Každá má NEZÁVISLÝ state!Kedy použiť:
typescript
✅ Form state
✅ Component-specific UI state
✅ Wizard/stepper state
✅ Modal/dialog state
✅ Temporary data editing
❌ Shared state between components
❌ State that survives navigation
❌ Global app stateStrategy 3: Provided in Route
typescript
// Route configuration
const routes: Routes = [
{
path: 'products',
component: ProductsPageComponent,
providers: [ProductsStore] // ← Provided for route!
}
];
// stores/products.store.ts
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';
interface Product {
id: string;
name: string;
price: number;
}
interface ProductsState {
products: Product[];
selectedId: string | null;
}
export const ProductsStore = signalStore(
withState<ProductsState>({
products: [],
selectedId: null
}),
withComputed(({ products, selectedId }) => ({
selectedProduct: computed(() => {
const id = selectedId();
return products().find(p => p.id === id) ?? null;
})
})),
withMethods((store) => ({
loadProducts(products: Product[]) {
patchState(store, { products });
},
selectProduct(id: string) {
patchState(store, { selectedId: id });
},
clearSelection() {
patchState(store, { selectedId: null });
}
}))
);
// products-page.component.ts
@Component({
selector: 'app-products-page',
standalone: true,
template: `
<app-product-list [products]="store.products$()" />
<app-product-detail [product]="store.selectedProduct$()" />
`
})
export class ProductsPageComponent {
readonly store = inject(ProductsStore);
}
// product-list.component.ts (child)
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<div *ngFor="let product of products">
<button (click)="store.selectProduct(product.id)">
{{ product.name }}
</button>
</div>
`
})
export class ProductListComponent {
@Input() products: Product[] = [];
readonly store = inject(ProductsStore); // Same instance from route!
}Charakteristiky:
typescript
scope: "Route component + všetky child components"
lifecycle: "Route activate → route deactivate"
sharing: "Všetky komponenty v route tree zdieľajú store"
// Navigation flow:
/products → ProductsStore created
├─ ProductsPageComponent (uses store)
├─ ProductListComponent (uses SAME store)
└─ ProductDetailComponent (uses SAME store)
/about → ProductsStore DESTROYED
/products → NEW ProductsStore createdKedy použiť:
typescript
✅ Feature-specific state
✅ Master-detail patterns
✅ Shared state within route
✅ Data loaded for entire section
❌ State needed across routes
❌ Global app state
❌ Independent component stateV ďalšej časti sa pozrieme na Real-World Patterns: Store s HTTP, LocalStorage, Entities a Composition.