Hook — 05 : 52 a.m., Medellín ↔ Santo Domingo live-debug
My tiny balcony swayed in the mountain breeze while Luis, our newest teammate in the Dominican Republic, screenshared a flickering stock-trading dashboard. “One tab updates, the others stay stale,” he groaned. Each component held its ownBehaviorSubject
; WebSocket events splintered across services. Five minutes, one cortado, and a crash-course in NgRx later, we funneled every tick into a single Store, dispatched typed Actions, and let an Effect handle API retries. The UI synced perfectly—even on my flaky Colombian café Wi-Fi. That dawn rescue is today’s roadmap: by the end you’ll know how to wire Store → Actions → Reducers → Selectors → Effects, spot common pitfalls, and keep global state rock-solid from Mexico City coworks to Brazilian beach hubs.
Why NgRx Still Matters in 2025
Pain Point | Real-World Cost | NgRx Fix |
---|---|---|
Local component state sprawls | Race conditions, stale views | Single immutable Store |
Duplicated API calls | Extra bandwidth on 3 G | Effects cache and dedupe requests |
Debugging across time zones | “Works on my machine” chaos | Redux DevTools time-travel |
Accessibility of async flows | Spinner flicker, lost ARIA states | Selectors derive clean loading/error flags |
NgRx adds boilerplate up-front but pays dividends when your app, team, or caffeine intake scales.
Concept Check—Three Pillars in Plain English
- Store: one read-only object tree representing UI state.
- Action: plain JS object describing “something happened.”
- Effect: side-effect handler (HTTP, WebSocket) that reacts to actions and dispatches new ones.
Analogy: In a salsa band, the Store is the sheet music (truth), Actions are the cues, and Effects are the percussionist hitting extra accents when cued.
Step-by-Step Walkthrough
1 — Install & Scaffold
bashCopyEditng add @ngrx/store@latest
ng add @ngrx/effects@latest
Generates app.reducer.ts
, registers StoreModule.forRoot(reducers)
in bootstrapApplication
.
2 — Design the State Shape
tsCopyEdit// src/app/state/portfolio.state.ts
export interface Holding { symbol: string; shares: number; price: number; }
export interface PortfolioState {
holdings: Holding[];
loading: boolean;
error?: string;
}
3 — Create Actions
tsCopyEditimport { createAction, props } from '@ngrx/store';
import { Holding } from './portfolio.state';
export const loadHoldings = createAction('[Portfolio] Load');
export const loadSuccess = createAction(
'[Portfolio] Load Success',
props<{ holdings: Holding[] }>()
);
export const loadFailure = createAction(
'[Portfolio] Load Failure',
props<{ error: string }>()
);
Tip – naming convention [Feature] Verb
keeps logs searchable across time zones.
4 — Write the Reducer
tsCopyEditimport { createReducer, on } from '@ngrx/store';
import { PortfolioState } from './portfolio.state';
import * as PortfolioActions from './portfolio.actions';
const initial: PortfolioState = {
holdings: [],
loading: false,
};
export const portfolioReducer = createReducer(
initial,
on(PortfolioActions.loadHoldings, s => ({ ...s, loading: true, error: undefined })),
on(PortfolioActions.loadSuccess, (s, { holdings }) => ({ ...s, holdings, loading: false })),
on(PortfolioActions.loadFailure, (s, { error }) => ({ ...s, error, loading: false }))
);
Reducer = pure: no dates, no console.logs, no HTTP.
5 — Select Smart Data
tsCopyEditimport { createSelector } from '@ngrx/store';
export const selectPortfolio = (s: AppState) => s.portfolio;
export const selectHoldings = createSelector(selectPortfolio, s => s.holdings);
export const selectTotal = createSelector(selectHoldings,
h => h.reduce((acc, x) => acc + x.shares * x.price, 0)
);
Components subscribe:
tsCopyEditholdings$ = this.store.select(selectHoldings);
total$ = this.store.select(selectTotal);
Angular’s async
pipe handles unsubscribe; no memory leaks during those long coffee breaks.
6 — Wire an Effect for HTTP
tsCopyEdit@Injectable()
export class PortfolioEffects {
load$ = createEffect(() =>
this.actions$.pipe(
ofType(PortfolioActions.loadHoldings),
switchMap(() =>
this.http.get<Holding[]>('/api/holdings').pipe(
map(data => PortfolioActions.loadSuccess({ holdings: data })),
catchError(err => of(PortfolioActions.loadFailure({ error: err.message })))
)
)
)
);
constructor(private actions$: Actions, private http: HttpClient) {}
}
Register:
tsCopyEditimportProvidersFrom(EffectsModule.forRoot([PortfolioEffects]))
Live Coding Example—Toggling Dark Mode
tsCopyEdit// actions
export const toggleTheme = createAction('[Settings] Toggle Theme');
// reducer
export interface UiState { dark: boolean; }
export const uiReducer = createReducer<UiState>(
{ dark: matchMedia('(prefers-color-scheme: dark)').matches },
on(toggleTheme, s => ({ ...s, dark: !s.dark }))
);
// component
<button (click)="store.dispatch(toggleTheme())">
Toggle Theme
</button>
CSS variables swap instantly; no localStorage juggling needed (an Effect can persist later).
Remote-Work Insight 🏝️
While hacking in Costa Rica, we needed offline support. An Effect listened for the
offline
browser event, fetched cached data from IndexedDB, and dispatched aloadSuccess
. My teammates in Panama saw zero downtime, even when a tropical storm cut the conference hotel’s fiber.
Performance & Accessibility Checkpoints
- Redux DevTools → Trace — Ensure one action per intent; batch UI events.
- Zone Profiling — NgRx actions outside Angular zone? Use
runInInjectionContext
to avoid missed change detection. - Bundle Size — Enable “Standalone APIs + functional providers” to tree-shake unused reducers.
- ARIA Live Regions — Derive loading/error flags via selectors; screen readers announce state changes once.
Common Pitfalls & Fixes
Pitfall | Symptom | Quick Fix |
---|---|---|
Dispatching too often | Janky UI | Debounce in component or use concatLatestFrom in Effect |
Mutating state in reducer | DevTools state tree freezes | Always return new object with spread ({ ...s } ) |
Multiple subscriptions | Memory leak | Use async pipe; avoid manual subscribe |
Race condition API calls | Older response overwrites new | Use switchMap or exhaustMap wisely |
CLI Cheat-Sheet
Command | Purpose |
---|---|
ng add @ngrx/store-devtools | Time-travel debugger |
ng g feature portfolio --module app --creators | Scaffold actions/reducer/selectors |
ng g effect portfolio --module app --flat | Create Effect |
ngrx/data | Rapid entity CRUD setup |
SVG Diagram Idea
User action → Dispatch Action → Reducer updates Store → Selectors emit → Component template updates (Change Detection). Parallel: Action → Effect → API → Success/Failure Action.
Wrap-Up
NgRx may seem verbose, but it’s a shared language that pays rent every time a new teammate joins from another time zone. Keep state pure in reducers, side effects in Effects, and UI logic in Selectors. With those guardrails—and a strong café con leche—you’ll wrangle complexity whether your app serves Colombian fintech or Mexican e-commerce. Drop your NgRx wins or woes below; I’ll reply between flights and late-night arepa sessions.