Angular 82 🅰️ State Management with NgRx
The previous chapter covered state management with services and signals. For many applications, that is sufficient. A service with a private signal, a readonly view, and mutation methods handles shared state cleanly. But as applications grow — multiple teams, complex workflows, undo/redo, time-travel debugging, state that must be persisted and rehydrated — the service pattern begins to strain. NgRx is the Angular ecosystem’s answer to that strain.
NgRx is a state management library built on the Redux pattern. It stores the entire application state in a single object, called the store. State changes are not made directly; they are dispatched as actions and processed by reducers, which produce a new state. Side effects — HTTP calls, local storage, WebSocket messages — are handled by effects. Selectors provide memoized views of the store. The pattern is disciplined, verbose, and predictable. The trade-off is code: NgRx requires more boilerplate than a service with signals. The benefit is a single source of truth, a traceable history of every state change, and tools that make complex state manageable .
Key point: NgRx is not a replacement for services. It is a layer on top of them. The store is provided by provideStore(). Actions are dispatched with store.dispatch(). Reducers are pure functions that take the current state and an action and return a new state. Effects are injectable classes that listen for actions and dispatch new ones. Selectors are memoized functions that derive values from the store. The whole system is built on RxJS observables and Angular’s dependency injection .
Why NgRx exists
A service with signals handles the common case. NgRx handles the cases that the service pattern does not.
The single-source-of-truth problem. In a service-based application, state is spread across multiple services. The cart service holds the cart, the user service holds the user, the notification service holds the notifications. Each one is independent. When they need to interact — a user logs out and the cart must be cleared — the services must know about each other. NgRx stores all state in one place. The store is the single source of truth, and any part of the application can read any part of the state.
The traceability problem. A service method mutates a signal, and the signal changes. There is no record of what changed it, when, or why. NgRx actions are records. Every state change is caused by an action, and every action is logged in the Redux DevTools. You can replay the actions, step backward and forward through the state history, and see exactly what happened. This is invaluable for debugging complex workflows.
The side-effect problem. A service that loads data must handle loading state, error state, retries, cancellation, and race conditions. It does this with imperative code that is hard to test. NgRx effects handle side effects declaratively. An effect listens for an action, performs the async work, and dispatches a success or failure action. The loading and error state are managed by the reducer, not by hand-written flags .
The testability problem. Reducers are pure functions. Given a state and an action, they return a new state. No HTTP, no timers, no DOM. They are the easiest thing in the application to test. Effects are tested with marble diagrams. Selectors are pure functions of the state. The whole system is testable without mocking anything except the HTTP layer.
The trade-off. NgRx adds ceremony. Every state change requires an action, a reducer case, and often an effect. A simple feature that would be twenty lines with a service becomes a hundred lines with NgRx. The library is worth adopting when the application has complex state interactions, multiple teams working on the same state, or a need for time-travel debugging. For a small application, it is overhead.
a. The Store, Actions, and Reducers
The NgRx store is a single object that holds the application state. The state is divided into slices, one per feature. Each slice is managed by a reducer. The reducer is a pure function that takes the current state and an action, and returns a new state.
The action is a plain object with a type property and an optional payload. NgRx provides createAction to define actions with type inference.
// counter.actions.ts
import { createAction, props } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
export const setCount = createAction(
'[Counter] Set Count',
props<{ count: number }>()
);
The string in brackets is a namespace that identifies the source of the action. The convention is [Feature] Event. The props<{ count: number }>() declares the payload type. Actions created by createAction return a function that produces an action object with a type property and, when props is used, a payload.
The reducer uses createReducer to map actions to state transitions:
// counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';
export interface CounterState {
count: number;
}
export const initialState: CounterState = {
count: 0,
};
export const counterReducer = createReducer(
initialState,
on(CounterActions.increment, (state) => ({
...state,
count: state.count + 1,
})),
on(CounterActions.decrement, (state) => ({
...state,
count: state.count - 1,
})),
on(CounterActions.reset, () => initialState),
on(CounterActions.setCount, (state, { count }) => ({
...state,
count,
}))
);
The on function maps an action to a handler. The handler receives the current state and the action’s payload and returns the new state. The spread operator ...state copies the existing state so that only the changed property is replaced. Reducers must be pure: they must not mutate the state, must not call side effects, and must return a new object.
The reducer is registered with the store in the application configuration:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ counter: counterReducer }),
],
};
The provideStore function registers the reducer under the key counter. The store’s state is now { counter: { count: 0 } }.
A component dispatches actions and reads state through selectors:
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
import { selectCount } from './counter.selectors';
@Component({
selector: 'counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">−</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
private store = inject(Store);
count = this.store.selectSignal(selectCount);
increment() { this.store.dispatch(increment()); }
decrement() { this.store.dispatch(decrement()); }
reset() { this.store.dispatch(reset()); }
}
The store.selectSignal method returns a signal that updates when the selected value changes. This is the modern way to consume the store in Angular templates. The store.dispatch method sends the action to the reducer.
b. Selectors: Deriving State
Selectors are pure functions that take the store’s state and return a derived value. They are memoized: if the input state has not changed, the selector returns the cached result instead of recomputing. This is essential for performance in a store with many subscribers.
The createFeatureSelector function selects a slice of the state. The createSelector function composes selectors to derive new values.
// counter.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';
export const selectCounterState = createFeatureSelector<CounterState>('counter');
export const selectCount = createSelector(
selectCounterState,
(state) => state.count
);
export const selectIsPositive = createSelector(
selectCount,
(count) => count > 0
);
export const selectIsZero = createSelector(
selectCount,
(count) => count === 0
);
The selectCount selector reads the count property from the counter slice. The selectIsPositive selector depends on selectCount and returns a boolean. Because both selectors are memoized, selectIsPositive only recomputes when selectCount changes.
Selectors can also take arguments. This is useful for selecting an item from a collection by ID:
export const selectUserById = (id: string) =>
createSelector(selectUsers, (users) => users.find(u => u.id === id));
The outer function takes the argument and returns the memoized selector. Each call with a different id creates a new selector with its own cache.
For feature modules loaded lazily, createFeature provides a grouped API that combines the feature name, reducer, and selectors. This is the modern pattern for NgRx features.
// counter.feature.ts
import { createFeature, createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';
export interface CounterState {
count: number;
}
const initialState: CounterState = { count: 0 };
export const counterFeature = createFeature({
name: 'counter',
reducer: createReducer(
initialState,
on(CounterActions.increment, (state) => ({ ...state, count: state.count + 1 })),
on(CounterActions.decrement, (state) => ({ ...state, count: state.count - 1 })),
on(CounterActions.reset, () => initialState),
on(CounterActions.setCount, (state, { count }) => ({ ...state, count }))
),
});
// The feature automatically creates:
// counterFeature.name → 'counter'
// counterFeature.reducer → the reducer
// counterFeature.selectCount → a selector for the count
// counterFeature.selectState → a selector for the entire slice
The createFeature API eliminates the manual creation of the feature selector and the individual selectors. It is the recommended pattern for new NgRx code .
c. Effects: Handling Side Effects
An effect is an injectable class that listens for actions, performs side effects, and dispatches new actions. Effects are the place for HTTP calls, local storage operations, router navigation, and any other interaction with the outside world. Reducers must be pure; effects are the opposite — they exist precisely to handle impurity.
The createEffect function defines an effect. The effect is a function that returns an observable. The observable emits new actions, which the store dispatches.
// counter.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import * as CounterActions from './counter.actions';
import { CounterService } from './counter.service';
@Injectable()
export class CounterEffects {
private actions$ = inject(Actions);
private counterService = inject(CounterService);
loadCount$ = createEffect(() =>
this.actions$.pipe(
ofType(CounterActions.loadCount),
mergeMap(() =>
this.counterService.getCount().pipe(
map((count) => CounterActions.loadCountSuccess({ count })),
catchError((error) => of(CounterActions.loadCountFailure({ error })))
)
)
)
);
}
The Actions service is an observable of all dispatched actions. The ofType operator filters for the actions the effect cares about. The mergeMap operator flattens the inner observable (the HTTP call). The map operator transforms the response into a success action. The catchError operator transforms an error into a failure action.
The effect is registered with provideEffects in the application configuration:
// app.config.ts
import { provideEffects } from '@ngrx/effects';
import { CounterEffects } from './counter.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ counter: counterReducer }),
provideEffects([CounterEffects]),
],
};
Effects use RxJS operators that determine how concurrent actions are handled. The choice of operator matters:
| Operator | Behavior |
|---|---|
mergeMap | Run all in parallel |
concatMap | Run in order, one at a time |
switchMap | Cancel previous, run latest |
exhaustMap | Ignore new while one is running |
The choice depends on the operation. switchMap is used for search-as-you-type, where only the latest request matters. concatMap is used for operations that must run in order, like saving a sequence of changes. exhaustMap is used for operations that should not be triggered again until the current one finishes, like a login form.
Effects can also be non-dispatching. The { dispatch: false } option tells NgRx that the effect does not dispatch an action. This is useful for effects that only perform a side effect, like logging or navigation.
logActions$ = createEffect(() =>
this.actions$.pipe(
tap((action) => console.log('Action dispatched:', action))
),
{ dispatch: false }
);
Complete Example Session
This session builds a counter feature with actions, a reducer, selectors, effects, and a component that consumes them.
// ============================================
// PART 1: THE ACTIONS
// ============================================
// counter.actions.ts
import { createAction, props } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
export const setCount = createAction(
'[Counter] Set Count',
props<{ count: number }>()
);
export const loadCount = createAction('[Counter] Load Count');
export const loadCountSuccess = createAction(
'[Counter] Load Count Success',
props<{ count: number }>()
);
export const loadCountFailure = createAction(
'[Counter] Load Count Failure',
props<{ error: string }>()
);
// ============================================
// PART 2: THE REDUCER
// ============================================
// counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';
export interface CounterState {
count: number;
loading: boolean;
error: string | null;
}
export const initialState: CounterState = {
count: 0,
loading: false,
error: null,
};
export const counterReducer = createReducer(
initialState,
on(CounterActions.increment, (state) => ({
...state,
count: state.count + 1,
})),
on(CounterActions.decrement, (state) => ({
...state,
count: state.count - 1,
})),
on(CounterActions.reset, () => initialState),
on(CounterActions.setCount, (state, { count }) => ({
...state,
count,
})),
on(CounterActions.loadCount, (state) => ({
...state,
loading: true,
error: null,
})),
on(CounterActions.loadCountSuccess, (state, { count }) => ({
...state,
count,
loading: false,
})),
on(CounterActions.loadCountFailure, (state, { error }) => ({
...state,
loading: false,
error,
}))
);
// ============================================
// PART 3: THE FEATURE
// ============================================
// counter.feature.ts
import { createFeature } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
export const counterFeature = createFeature({
name: 'counter',
reducer: counterReducer,
});
// Automatically creates:
// counterFeature.selectCount
// counterFeature.selectLoading
// counterFeature.selectError
// ============================================
// PART 4: THE SELECTORS
// ============================================
// counter.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';
export const selectCounterState = createFeatureSelector<CounterState>('counter');
export const selectCount = createSelector(
selectCounterState,
(state) => state.count
);
export const selectLoading = createSelector(
selectCounterState,
(state) => state.loading
);
export const selectIsPositive = createSelector(
selectCount,
(count) => count > 0
);
export const selectDisplay = createSelector(
selectCount,
selectLoading,
(count, loading) => loading ? '...' : `Count: ${count}`
);
// ============================================
// PART 5: THE SERVICE
// ============================================
// counter.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CounterService {
private http = inject(HttpClient);
getCount(): Observable<number> {
return this.http.get<number>('/api/counter');
}
}
// ============================================
// PART 6: THE EFFECTS
// ============================================
// counter.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import * as CounterActions from './counter.actions';
import { CounterService } from './counter.service';
@Injectable()
export class CounterEffects {
private actions$ = inject(Actions);
private counterService = inject(CounterService);
loadCount$ = createEffect(() =>
this.actions$.pipe(
ofType(CounterActions.loadCount),
mergeMap(() =>
this.counterService.getCount().pipe(
map((count) => CounterActions.loadCountSuccess({ count })),
catchError((error) =>
of(CounterActions.loadCountFailure({ error: error.message }))
)
)
)
)
);
}
// ============================================
// PART 7: THE APPLICATION CONFIG
// ============================================
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { counterReducer } from './counter.reducer';
import { CounterEffects } from './counter.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ counter: counterReducer }),
provideEffects([CounterEffects]),
],
};
// ============================================
// PART 8: THE COMPONENT
// ============================================
// counter.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as CounterActions from './counter.actions';
import { selectCount, selectLoading, selectError } from './counter.selectors';
@Component({
selector: 'counter',
template: `
<p>Count: {{ count() }}</p>
@if (loading()) {
<p>Loading...</p>
}
@if (error()) {
<p class="error">{{ error() }}</p>
}
<button (click)="increment()">+</button>
<button (click)="decrement()">−</button>
<button (click)="reset()">Reset</button>
<button (click)="load()">Load</button>
`,
})
export class CounterComponent implements OnInit {
private store = inject(Store);
count = this.store.selectSignal(selectCount);
loading = this.store.selectSignal(selectLoading);
error = this.store.selectSignal(selectError);
ngOnInit() {
this.store.dispatch(CounterActions.loadCount());
}
increment() { this.store.dispatch(CounterActions.increment()); }
decrement() { this.store.dispatch(CounterActions.decrement()); }
reset() { this.store.dispatch(CounterActions.reset()); }
load() { this.store.dispatch(CounterActions.loadCount()); }
}
// ============================================
// PART 9: THE PROVIDE STORE FOR FEATURES
// ============================================
// For lazily-loaded routes:
import { provideState } from '@ngrx/store';
import { counterFeature } from './counter.feature';
export const routes: Routes = [
{
path: 'counter',
loadComponent: () =>
import('./counter.component').then(m => m.CounterComponent),
providers: [
provideState(counterFeature),
provideEffects([CounterEffects]),
],
},
];
// The feature's reducer is registered when the route loads.
// It is unregistered when the route is destroyed.
// ============================================
// PART 10: THE TEST FOR THE REDUCER
// ============================================
// counter.reducer.spec.ts
import { counterReducer, initialState } from './counter.reducer';
import * as CounterActions from './counter.actions';
describe('counterReducer', () => {
it('increments the count', () => {
const action = CounterActions.increment();
const state = counterReducer(initialState, action);
expect(state.count).toBe(1);
});
it('resets to initial state', () => {
const stateWithCount = { ...initialState, count: 5 };
const action = CounterActions.reset();
const state = counterReducer(stateWithCount, action);
expect(state.count).toBe(0);
});
it('sets loading on loadCount', () => {
const action = CounterActions.loadCount();
const state = counterReducer(initialState, action);
expect(state.loading).toBe(true);
});
});
The ten parts cover the actions, the reducer, the feature, the selectors, the service, the effects, the application config, the component, the lazy-loaded feature registration, and the reducer test.
Quick Reference
The NgRx Building Blocks
| Building Block | Purpose |
|---|---|
| Store | Single source of truth |
| Action | Declares an event that happened |
| Reducer | Pure function from state + action to new state |
| Selector | Memoized function from state to derived value |
| Effect | Handles side effects and dispatches new actions |
| Feature | Groups reducer and selectors under a name |
The Core Functions
| Function | Purpose |
|---|---|
createAction | Define an action |
createReducer | Define a reducer from action handlers |
on | Map an action to a state transition |
createSelector | Define a memoized selector |
createFeatureSelector | Select a slice of the store |
createFeature | Group reducer and selectors |
createEffect | Define a side effect |
ofType | Filter actions by type |
provideStore | Register the root store |
provideState | Register a feature state |
provideEffects | Register effects |
The RxJS Operators in Effects
| Operator | Behavior |
|---|---|
mergeMap | Run all in parallel |
concatMap | Run in order, one at a time |
switchMap | Cancel previous, run latest |
exhaustMap | Ignore new while one runs |
catchError | Handle errors |
map | Transform the value |
tap | Side effect without changing the value |
The State Access Patterns
| Method | Returns |
|---|---|
store.select(selector) | Observable |
store.selectSignal(selector) | Signal |
store.dispatch(action) | void |
store.pipe(...) | Observable |
Best Practices
✅ Do This:
// Use createFeature for new features
export const counterFeature = createFeature({ name: 'counter', reducer }); // ✅
// Use selectSignal in components
count = this.store.selectSignal(selectCount); // ✅
// Keep reducers pure
on(increment, (state) => ({ ...state, count: state.count + 1 })) // ✅
// Use createAction with props for payloads
export const setCount = createAction('[Counter] Set', props<{ count: number }>()); // ✅
// Use switchMap for search, concatMap for saves
ofType(loadCount), mergeMap(() => ...) // ✅
// Register effects with provideEffects
provideEffects([CounterEffects]) // ✅
❌ Don’t Do This:
// Don't mutate state in the reducer
on(increment, (state) => { state.count++; return state; }) // ❌
// Don't call HTTP in a reducer
on(load, () => this.http.get(...)) // ❌
// Don't use store.select without async pipe or selectSignal
count$ = this.store.select(selectCount); // must subscribe // ❌
// Don't dispatch actions from the reducer
on(load, (state) => { this.store.dispatch(...); return state; }) // ❌
// Don't use mergeMap for save operations
mergeMap(() => this.save()) // may run in parallel // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| State doesn’t update | Reducer mutated instead of returning new state | Spread the state |
| Selector recomputes always | New selector created inside a function | Define the selector outside |
| HTTP call repeats | Effect dispatches an action it also listens to | Use separate success/failure actions |
| Actions never fire | Effect not registered | Add to provideEffects |
| Memory leak in component | Manual subscription without unsubscribe | Use selectSignal |
| Race condition | mergeMap runs in parallel | Use concatMap or switchMap |
Real-World Examples
1. Action with Payload
export const setCount = createAction('[Counter] Set', props<{ count: number }>());
2. Reducer Case
on(setCount, (state, { count }) => ({ ...state, count }))
3. Feature Selector
export const selectCount = createSelector(selectCounter, (s) => s.count);
4. Component with SelectSignal
count = this.store.selectSignal(selectCount);
5. Dispatch from Component
this.store.dispatch(increment());
6. Effect for HTTP
loadCount$ = createEffect(() => this.actions$.pipe(ofType(loadCount), mergeMap(...)));
7. Register Store
provideStore({ counter: counterReducer })
8. Register Effects
provideEffects([CounterEffects])
9. Lazy Feature State
providers: [provideState(counterFeature), provideEffects([CounterEffects])]
10. Reducer Test
const state = counterReducer(initialState, increment());
expect(state.count).toBe(1);
Visual
The NgRx Data Flow
┌──────────────────────────────────────────────┐
│ NGRX FLOW │
│ │
│ Component │
│ │ dispatch(action) │
│ ▼ │
│ Store │
│ │ │
│ ├──> Reducer ──> new state │
│ │ │
│ └──> Effect ──> HTTP call ──> new action │
│ │ │
│ └──> dispatch(success) ──> Reducer
│ │
│ Selector │
│ │ │
│ ▼ │
│ Component (selectSignal) │
│ │
└──────────────────────────────────────────────┘
The Store Structure
┌──────────────────────────────────────────────┐
│ STORE STATE │
│ │
│ { │
│ counter: { │
│ count: 0, │
│ loading: false, │
│ error: null │
│ }, │
│ users: { │
│ list: [], │
│ selected: null │
│ }, │
│ cart: { │
│ items: [], │
│ total: 0 │
│ } │
│ } │
│ │
│ Each key is a feature slice. │
│ Each slice has its own reducer. │
│ │
└──────────────────────────────────────────────┘
The Effect Operator Choice
┌──────────────────────────────────────────────┐
│ EFFECT OPERATORS │
│ │
│ mergeMap: │
│ → Runs all in parallel │
│ → Use for independent requests │
│ │
│ concatMap: │
│ → Runs one at a time, in order │
│ → Use for saves, ordered operations │
│ │
│ switchMap: │
│ → Cancels previous, uses latest │
│ → Use for search-as-you-type │
│ │
│ exhaustMap: │
│ → Ignores new while one runs │
│ → Use for login, prevent double-submit │
│ │
└──────────────────────────────────────────────┘
The Testing Pyramid for NgRx
┌──────────────────────────────────────────────┐
│ NGRX TESTING │
│ │
│ Selectors (easiest): │
│ └─ Pure functions of state │
│ └─ No setup, no mocks │
│ │
│ Reducers: │
│ └─ Pure functions of (state, action) │
│ └─ Test every action case │
│ │
│ Effects: │
│ └─ Marble diagrams with TestScheduler │
│ └─ Mock HTTP, verify dispatched actions │
│ │
│ Components (hardest): │
│ └─ Mock the Store with provideMockStore │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Store | Single source of truth |
| Action | Event with type and optional payload |
| Reducer | Pure function (state, action) => newState |
| Selector | Memoized (state) => derived value |
| Effect | Side effect handler that dispatches actions |
| Feature | Grouped reducer and selectors |
| Root registration | provideStore({ feature: reducer }) |
| Lazy registration | provideState(feature) |
| Effect registration | provideEffects([Effects]) |
| Component access | store.selectSignal(selector) |
| Dispatch | store.dispatch(action) |
| Effect operator | mergeMap, concatMap, switchMap, exhaustMap |
Key takeaways:
- NgRx stores all application state in a single store. The store is an object with one key per feature. Each feature’s slice is managed by a reducer. The store is the single source of truth .
- Actions describe what happened. They are dispatched from components and effects. Every state change is caused by an action. The Redux DevTools log every action, enabling time-travel debugging.
- Reducers are pure functions. They take the current state and an action, and return a new state. They must not mutate the existing state, must not perform side effects, and must not dispatch new actions.
- Selectors are memoized functions. They read from the store and return derived values. Memoization ensures that a selector only recomputes when its input changes.
createFeaturegenerates selectors automatically for common cases . - Effects handle side effects. They listen for actions with
ofType, perform async operations, and dispatch success or failure actions. The RxJS operator choice determines concurrency behavior:mergeMapfor parallel,concatMapfor sequential,switchMapfor cancel-previous,exhaustMapfor ignore-while-busy. - Use
provideStorefor the root andprovideStatefor features. The root store is registered in the application config. Feature stores are registered in route providers for lazy-loaded features. They are unregistered when the route is destroyed. - Consume the store with
selectSignalin components. Thestore.selectSignal(selector)method returns a signal that updates when the selected value changes. This is the modern Angular way and avoids manual subscription management.
Remember: NgRx is not a better version of services. It is a different set of trade-offs. Services are simple, direct, and imperative. NgRx is structured, traceable, and declarative. Use services when the state is local and the interactions are simple. Use NgRx when the state is shared across features, when multiple teams work on the same state, when undo/redo is required, or when time-travel debugging is valuable. The ceremony is real. So is the clarity. Choose deliberately.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!