If this story helps make NgRx clear and simple, feel free to like or share—it might just help someone else, too!
I’m running a farm with a bunch of fields, and each field is growing a different crop. I’ve got wheat in one, corn in another, and a little pond for fish. It’s a lot to manage, so I hire a farm manager—that’s NgRx in this analogy.
Now, my job is to tell the farm manager what needs to happen. If I need the corn watered, I don’t personally run out with a watering can—I write a note saying “Water the corn” and hand it to the manager. That note? That’s an action in NgRx. It’s just a description of what I want to happen.
The farm manager doesn’t water the corn directly. Instead, they look at the farm’s playbook—the reducer—which tells them how to update the field based on the action. The reducer doesn’t water the corn itself, either—it just updates the farm’s ledger, called the store, saying, “Corn was watered.” This ledger keeps track of the entire farm’s state: what’s planted, what’s watered, what’s harvested.
But here’s the magical part. Every field worker on the farm who’s keeping an eye on the ledger immediately knows when something changes. That’s because of selectors—they’re like workers who only pay attention to the specific parts of the ledger they care about. So, if I’ve got a chef waiting for fresh corn, they’ll know instantly when the corn is ready to pick.
NgRx is like this efficient system where I don’t have to run around the farm myself. It keeps everything organized, predictable, and ensures everyone is on the same page about what’s happening. Without it, managing the farm—or a complex Angular app—would be chaos.
1. Actions: The Notes You Hand to the Manager
An action is just a JavaScript object with a type and optionally some payload. Here’s how we write one:
import { createAction, props } from '@ngrx/store';
export const waterCorn = createAction(
'[Farm] Water Corn',
props<{ amount: number }>() // Payload to specify the amount of water
);
This waterCorn
action describes what needs to happen but doesn’t do the work itself.
2. Reducers: The Playbook for the Manager
The reducer is a pure function that knows how to update the state (the farm’s ledger) based on the action it receives.
import { createReducer, on } from '@ngrx/store';
import { waterCorn } from './farm.actions';
export interface FarmState {
cornWatered: number;
}
const initialState: FarmState = {
cornWatered: 0,
};
export const farmReducer = createReducer(
initialState,
on(waterCorn, (state, { amount }) => ({
...state,
cornWatered: state.cornWatered + amount,
}))
);
This reducer listens for the waterCorn
action and updates the state.
3. Selectors: The Workers Who Watch the Ledger
Selectors are functions that extract specific pieces of state. This is how we get data like the current amount of water the corn has received.
import { createSelector } from '@ngrx/store';
export const selectFarmState = (state: any) => state.farm;
export const selectCornWatered = createSelector(
selectFarmState,
(farmState) => farmState.cornWatered
);
By using selectors, Angular components don’t need to worry about the whole ledger—they only get the part they need.
4. Dispatching Actions: Handing the Note
In Angular, we dispatch actions using the Store
service. This is like handing a note to the farm manager.
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { waterCorn } from './farm.actions';
@Component({
selector: 'app-farm',
template: `<button (click)="water()">Water Corn</button>`,
})
export class FarmComponent {
constructor(private store: Store) {}
water() {
this.store.dispatch(waterCorn({ amount: 5 }));
}
}
5. Subscribing to State Changes: Workers Watching the Fields
Components can subscribe to selectors to react to changes in the state.
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCornWatered } from './farm.selectors';
@Component({
selector: 'app-farm-status',
template: `<p>Corn watered: {{ cornWatered$ | async }}</p>`,
})
export class FarmStatusComponent {
cornWatered$ = this.store.select(selectCornWatered);
}
The cornWatered$
observable automatically updates when the state changes.
Key Takeaways / Final Thoughts
- Predictability: The reducer ensures state changes are consistent and predictable.
- Centralized State: The store is a single source of truth for your application.
- Reactivity: Selectors and observables make it easy for components to react to changes in the state.
- Scalability: As the app grows, NgRx keeps state management clean and maintainable.
NgRx might seem complex at first, but it provides structure to manage state effectively, especially in large Angular applications. With actions, reducers, selectors, and the store, it turns chaos into a well-oiled machine.