Obsah článku / Contents
NgRx Signal Store: Základy a Provider Strategies (Časť 1)

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 needed

Krok 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 state

Strategy 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 state

Strategy 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 created

Kedy 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 state

V ďalšej časti sa pozrieme na Real-World Patterns: Store s HTTP, LocalStorage, Entities a Composition.

Všetky články