NgRx is more than a store. It is an architecture for managing complex application state in Angular using predictable state transitions, reactive programming, and unidirectional data flow.
Modern Angular applications can start simple, but as the application grows, state management becomes increasingly difficult.
You may eventually have:
- Components sharing the same data
- Multiple API requests updating the same state
- Complex loading and error states
- Data that must survive navigation
- Business logic spread across components
- Race conditions between asynchronous operations
- Difficult-to-debug state changes
This is where NgRx becomes useful.
In this article, we will build the concepts from the ground up and gradually move toward real-world patterns.
Table of Contents
- What Is State Management?
- Why Do We Need NgRx?
- What Is NgRx?
- NgRx Architecture
- The Unidirectional Data Flow
- Store
- Actions
- Reducers
- Selectors
- Effects
- Dispatching Actions
- Reading State
- Complete CRUD Example
- Async Operations
- Loading and Error States
- Entity State
- NgRx Entity
- Feature-Based State
- ComponentStore vs Store
- Facades
- Immutability
- Common NgRx Patterns
- Common Mistakes
- Performance
- Testing
- When Should You Use NgRx?
- Final Architecture
1. What Is State Management?
Before understanding NgRx, we need to understand state.
State is simply the data that describes the current condition of your application.
For example:
interface User {
id: number;
name: string;
email: string;
}
Your application might have:
interface AppState {
user: User | null;
isLoading: boolean;
error: string | null;
}
At a particular moment:
{
user: {
id: 1,
name: 'John',
email: 'john@example.com'
},
isLoading: false,
error: null
}
This is application state.
2. Local State vs Global State
Not every piece of state needs NgRx.
Consider a button:
isMenuOpen = false;
This is usually local component state.
You probably don't need a global store for it.
But consider:
Authentication
Products
Shopping Cart
User Profile
Notifications
Permissions
Orders
These may be shared by many parts of the application.
For example:
Navbar
↓
User Authentication State
↑
Profile Page
↑
Checkout
↑
Orders
When many components depend on the same state, centralized state management becomes useful.
3. What Is NgRx?
NgRx is a collection of Angular libraries inspired by Redux and built around reactive programming with RxJS.
The core idea is:
Component
↓
Action
↓
Reducer / Effect
↓
Store
↓
Selector
↓
Component
Instead of allowing components to modify shared state directly, state changes happen through actions.
This creates predictable state transitions.
4. NgRx Architecture
The main pieces are:
┌─────────────┐
│ Component │
└──────┬──────┘
│
dispatch
│
▼
┌─────────────┐
│ Action │
└──────┬──────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Reducer │ │ Effect │
└────┬─────┘ └────┬─────┘
│ │
│ ▼
│ HTTP / API
│ │
│ ▼
│ Action
│ │
└─────────┬─────────┘
▼
┌──────────┐
│ Store │
└────┬─────┘
│
select
│
▼
┌─────────────┐
│ Component │
└─────────────┘
There are five concepts you should understand extremely well:
Actions
Reducers
Store
Selectors
Effects
5. The Unidirectional Data Flow
NgRx follows a unidirectional data flow.
That means data moves in a predictable direction.
UI
↓
Action
↓
State Transition
↓
Store
↓
Selector
↓
UI
For example:
this.store.dispatch(
increment()
);
The component does not directly change:
count = count + 1;
Instead, it describes what happened:
increment()
The reducer determines how the state should change.
6. The Store
The Store is the centralized state container.
Imagine:
interface AppState {
counter: number;
user: User | null;
products: Product[];
}
Conceptually:
Store
├── counter
├── user
└── products
The Store itself is observable.
You read state reactively:
counter$ = this.store.select(selectCounter);
Then:
<p>{{ counter$ | async }}</p>
7. Actions
An Action describes something that happened.
Example:
import { createAction } from '@ngrx/store';
export const increment = createAction(
'[Counter] Increment'
);
Another:
export const loadProducts = createAction(
'[Products Page] Load Products'
);
The string:
[Products Page] Load Products
is the action type.
A good action describes an event.
Prefer:
[Login Page] Login Submitted
over:
[Auth] Set User
Why?
Because the first describes what happened, while the second describes an implementation detail.
8. Actions With Payloads
Actions can carry data.
export const addToCart = createAction(
'[Cart] Add Product',
props<{ productId: number }>()
);
Dispatch:
this.store.dispatch(
addToCart({ productId: 10 })
);
Another example:
export const login = createAction(
'[Login Page] Login Submitted',
props<{
email: string;
password: string;
}>()
);
Then:
this.store.dispatch(
login({
email: 'john@example.com',
password: '123456'
})
);
9. Reducers
Reducers determine how state changes in response to actions.
Example:
import { createReducer, on } from '@ngrx/store';
export const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, state => state + 1)
);
The important concept is:
Action
+
Current State
↓
New State
For example:
State = 5
Action = increment
Reducer
5 + 1
New State = 6
10. Reducers Must Be Pure
A reducer should be:
- Predictable
- Synchronous
- Pure
- Free from side effects
Don't do this inside a reducer:
on(loadProducts, state => {
http.get('/products');
return state;
});
Reducers should not perform HTTP requests.
They should only calculate the next state.
11. Immutable State
NgRx relies heavily on immutability.
Bad:
state.user.name = 'John';
return state;
Good:
return {
...state,
user: {
...state.user,
name: 'John'
}
};
The idea is to create a new state reference instead of mutating the existing one.
12. Selectors
Selectors are used to read data from the Store.
Suppose:
interface CounterState {
count: number;
}
Create a feature selector:
export const selectCounterState =
createFeatureSelector<CounterState>('counter');
Then:
export const selectCount = createSelector(
selectCounterState,
state => state.count
);
Component:
count$ = this.store.select(selectCount);
Template:
<h1>
{{ count$ | async }}
</h1>
13. Why Selectors Are Important
You might ask:
Why not simply get the entire state?
Because selectors provide:
Encapsulation
Components don't need to know how state is structured.
Memoization
Selectors can avoid unnecessary recalculations.
Reusability
Multiple components can use the same selector.
Performance
Components subscribe only to the data they need.
14. Derived State
One of the most powerful selector features is derived state.
Suppose:
interface CartState {
items: CartItem[];
}
You can calculate:
export const selectCartTotal = createSelector(
selectCartItems,
items =>
items.reduce(
(total, item) => total + item.price * item.quantity,
0
)
);
The total does not need to be stored separately.
Instead:
Cart Items
↓
Selector
↓
Cart Total
This avoids duplicated state.
15. Effects
Effects handle side effects.
Examples:
HTTP Requests
Local Storage
Analytics
Navigation
WebSocket interactions
External APIs
Suppose:
User clicks Load Products
↓
loadProducts action
↓
Effect
↓
HTTP request
↓
API response
↓
loadProductsSuccess
↓
Reducer
↓
Store
This is the main purpose of NgRx Effects.
16. Example Effect
@Injectable()
export class ProductsEffects {
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(loadProducts),
switchMap(() =>
this.productsService.getProducts().pipe(
map(products =>
loadProductsSuccess({ products })
),
catchError(error =>
of(loadProductsFailure({ error }))
)
)
)
)
);
constructor(
private actions$: Actions,
private productsService: ProductsService
) {}
}
The effect listens for:
loadProducts
Then performs:
productsService.getProducts()
And dispatches:
loadProductsSuccess
or:
loadProductsFailure
17. The Complete Request Flow
Let's visualize it:
User
│
▼
Products Component
│
│ dispatch()
▼
loadProducts
│
▼
Products Effect
│
│ HTTP
▼
Backend API
│
▼
Response
│
▼
loadProductsSuccess
│
▼
Products Reducer
│
▼
Store
│
▼
selectProducts
│
▼
Products Component
This is one of the most important NgRx flows to understand.
18. Loading and Error State
A realistic application needs more than data.
For example:
interface ProductsState {
products: Product[];
loading: boolean;
error: string | null;
}
Initial state:
const initialState: ProductsState = {
products: [],
loading: false,
error: null
};
When loading starts:
on(loadProducts, state => ({
...state,
loading: true,
error: null
}))
Success:
on(loadProductsSuccess, (state, { products }) => ({
...state,
products,
loading: false
}))
Failure:
on(loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
19. Complete Products Example
Actions
export const loadProducts = createAction(
'[Products Page] Load Products'
);
export const loadProductsSuccess = createAction(
'[Products API] Load Products Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
'[Products API] Load Products Failure',
props<{ error: string }>()
);
Reducer
export interface ProductsState {
products: Product[];
loading: boolean;
error: string | null;
}
export const initialState: ProductsState = {
products: [],
loading: false,
error: null
};
export const productsReducer = createReducer(
initialState,
on(loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(loadProductsSuccess, (state, { products }) => ({
...state,
products,
loading: false
})),
on(loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
Selectors
export const selectProductsState =
createFeatureSelector<ProductsState>('products');
export const selectProducts = createSelector(
selectProductsState,
state => state.products
);
export const selectLoading = createSelector(
selectProductsState,
state => state.loading
);
export const selectError = createSelector(
selectProductsState,
state => state.error
);
Component
products$ = this.store.select(selectProducts);
loading$ = this.store.select(selectLoading);
error$ = this.store.select(selectError);
loadProducts() {
this.store.dispatch(loadProducts());
}
Template
<button (click)="loadProducts()">
Load Products
</button>
@if (loading$ | async) {
<p>Loading...</p>
}
@if (error$ | async; as error) {
<p>{{ error }}</p>
}
@for (product of products$ | async; track product.id) {
<article>
<h2>{{ product.name }}</h2>
<p>{{ product.price }}</p>
</article>
}
20. NgRx Entity
Imagine you have:
Product[]
with thousands of products.
You frequently need:
Find product by ID
Add product
Remove product
Update product
Select all products
Managing arrays manually becomes repetitive.
NgRx Entity provides utilities for normalized collections.
Conceptually:
Entity State
ids:
[1, 2, 3]
entities:
{
1: Product,
2: Product,
3: Product
}
Instead of:
products.find(p => p.id === id);
the entity adapter can provide selectors and operations for you.
21. Entity Adapter
Example:
const adapter = createEntityAdapter<Product>();
Initial state:
const initialState =
adapter.getInitialState({
loading: false,
error: null
});
Add products:
adapter.addMany(products, state);
Update:
adapter.updateOne(
{
id: product.id,
changes: product
},
state
);
Remove:
adapter.removeOne(productId, state);
This becomes extremely useful for large collections.
22. Feature-Based Architecture
A large Angular application should not have one giant Store file.
Instead:
src/app/
│
├── core/
│
├── shared/
│
└── features/
│
├── auth/
│ ├── store/
│ │ ├── auth.actions.ts
│ │ ├── auth.reducer.ts
│ │ ├── auth.effects.ts
│ │ └── auth.selectors.ts
│ │
│ └── pages/
│
├── products/
│ ├── store/
│ │ ├── products.actions.ts
│ │ ├── products.reducer.ts
│ │ ├── products.effects.ts
│ │ └── products.selectors.ts
│
└── cart/
├── store/
│ ├── cart.actions.ts
│ ├── cart.reducer.ts
│ ├── cart.effects.ts
│ └── cart.selectors.ts
This keeps features isolated.
23. NgRx Facade Pattern
Some teams don't want components to know about NgRx directly.
Instead, introduce a facade.
@Injectable({
providedIn: 'root'
})
export class ProductsFacade {
products$ = this.store.select(selectProducts);
loading$ = this.store.select(selectLoading);
constructor(
private store: Store
) {}
loadProducts() {
this.store.dispatch(loadProducts());
}
}
Component:
constructor(
private productsFacade: ProductsFacade
) {}
products$ = this.productsFacade.products$;
loadProducts() {
this.productsFacade.loadProducts();
}
Now the component doesn't know about:
Actions
Selectors
Store
It only knows:
ProductsFacade
This can be useful in large applications.
24. RxJS and NgRx
NgRx relies heavily on RxJS.
For example:
this.store.select(selectProducts)
returns an Observable.
Effects also use RxJS operators:
ofType()
switchMap()
mergeMap()
concatMap()
exhaustMap()
map()
catchError()
Understanding RxJS is therefore extremely valuable when working with NgRx.
25. Choosing the Correct Flattening Operator
This is a common interview topic.
switchMap
Cancels the previous request.
Useful for:
Search
Autocomplete
Example:
actions$.pipe(
ofType(searchProducts),
switchMap(...)
);
concatMap
Queues requests.
Request 1
↓
Request 2
↓
Request 3
Useful when order matters.
mergeMap
Runs requests concurrently.
Request 1 ────────┐
Request 2 ────────┤
Request 3 ────────┘
Useful when requests are independent.
exhaustMap
Ignores new requests while one is running.
Useful for:
Login
Submit button
Payment
For example:
actions$.pipe(
ofType(login),
exhaustMap(...)
);
If the user clicks Login multiple times quickly, subsequent clicks are ignored until the first request completes.
26. NgRx vs Services
You don't need NgRx for every Angular application.
A simple service may be enough:
@Injectable({
providedIn: 'root'
})
export class CartService {
private cartSubject =
new BehaviorSubject<CartItem[]>([]);
cart$ = this.cartSubject.asObservable();
add(item: CartItem) {
const current = this.cartSubject.value;
this.cartSubject.next([
...current,
item
]);
}
}
This can work perfectly well for a small application.
NgRx becomes more valuable when state transitions and interactions become complex.
27. NgRx vs ComponentStore
NgRx Store is designed for centralized application state.
ComponentStore is better suited for localized state.
Example:
Global Application State
↓
Store
Feature-local state
↓
ComponentStore
You don't necessarily need to choose one exclusively.
A large application can use both.
28. What Should NOT Go Into NgRx?
A common mistake is putting everything into the Store.
Don't automatically store:
isDropdownOpen
hoveredItem
temporary input values
modal visibility
simple UI flags
unless there is a real reason.
Ask:
Does this state need to be shared, persisted, coordinated, or observed across multiple parts of the application?
If not, local state may be better.
29. Avoid Duplicated State
Suppose you have:
products: Product[];
selectedProduct: Product;
You may be duplicating information.
Instead, consider:
products: Product[];
selectedProductId: number | null;
Then:
export const selectSelectedProduct = createSelector(
selectProducts,
selectSelectedProductId,
(products, id) =>
products.find(product => product.id === id) ?? null
);
Now there is one source of truth.
30. Keep Business Logic Out of Components
Bad:
loadProducts() {
this.loading = true;
this.http.get('/products')
.subscribe(products => {
this.products = products;
this.loading = false;
});
}
This mixes:
UI
HTTP
State
Loading
Error handling
With NgRx:
loadProducts() {
this.store.dispatch(loadProducts());
}
The component becomes much simpler.
31. Smart vs Presentational Components
NgRx works well with the separation between container and presentational components.
Container
Responsible for state interaction.
products$ = this.facade.products$;
Presentational
Receives data:
@Input()
products: Product[] = [];
and emits events:
@Output()
productSelected = new EventEmitter<number>();
Architecture:
Container
│
├── Store
│
└── Presentational Component
│
└── UI
32. Router Store
NgRx can also integrate router state.
This can allow application state to react to route changes.
Conceptually:
URL
↓
Router
↓
Router Store
↓
Selectors
↓
Application
This can be useful when route parameters influence application state.
33. DevTools
One of the major advantages of NgRx is debugging.
With Redux DevTools, you can inspect:
Action
↓
Previous State
↓
Next State
For example:
[Cart] Add Product
Previous:
items: []
Next:
items: [Product 1]
This makes complex state transitions much easier to understand.
34. Time-Travel Debugging
Because state changes are represented as actions, you can conceptually replay state transitions.
Action 1
Action 2
Action 3
Action 4
Instead of debugging:
"Something changed somewhere."
you can investigate:
"Which action caused the state transition?"
This is one of the architectural strengths of Redux-style state management.
35. Testing Reducers
Reducers are easy to test because they are pure functions.
Example:
it('should increment counter', () => {
const state = counterReducer(
5,
increment()
);
expect(state).toBe(6);
});
No HTTP request.
No browser.
No asynchronous operation.
Just:
Input
↓
Reducer
↓
Output
36. Testing Selectors
Selectors can also be tested independently.
For example:
const state = {
products: [
{ id: 1, price: 100 },
{ id: 2, price: 200 }
]
};
Then verify:
expect(selectProducts.projector(
state.products
)).toEqual(state.products);
Selectors are another reason to keep business calculations outside components.
37. Testing Effects
Effects require testing asynchronous behavior.
Conceptually:
Input Action
↓
Effect
↓
Mock API
↓
Output Action
You can verify:
loadProducts
↓
API called
↓
loadProductsSuccess
or:
loadProducts
↓
API error
↓
loadProductsFailure
38. Common NgRx Mistakes
Mistake 1 — Putting everything in Store
Not every variable belongs in global state.
Mistake 2 — Mutating state
Bad:
state.products.push(product);
return state;
Good:
return {
...state,
products: [
...state.products,
product
]
};
Mistake 3 — HTTP inside reducers
Never.
Use Effects or another appropriate side-effect mechanism.
Mistake 4 — Huge selectors
Selectors should remain focused and composable.
Mistake 5 — Components containing business logic
If your component becomes:
500+ lines
and heavily interacts with Store, consider extracting logic.
Mistake 6 — Overusing Effects
Effects are for side effects.
Don't create an effect just to transform simple state synchronously when a reducer or selector is enough.
39. NgRx and Angular Signals
Modern Angular applications increasingly use Signals.
You can bridge Store Observables into Signals.
For example:
products = this.store.selectSignal(selectProducts);
Then in the template:
@for (product of products(); track product.id) {
<p>{{ product.name }}</p>
}
This gives you a more signal-oriented component API while still using NgRx for centralized state management.
40. A Real-World E-Commerce Architecture
Imagine an e-commerce application.
We could structure the state as:
Application Store
│
├── Auth
│ ├── user
│ ├── token
│ └── loading
│
├── Products
│ ├── entities
│ ├── filters
│ ├── loading
│ └── error
│
├── Cart
│ ├── items
│ └── total
│
├── Orders
│ ├── entities
│ ├── loading
│ └── error
│
└── UI
├── notifications
└── theme
The flow could be:
User
│
▼
Angular Component
│
▼
Action
│
├──────────────┐
▼ ▼
Reducer Effect
│ │
▼ ▼
Store API
│ │
│ ▼
│ Success Action
│ │
└──────────────┘
│
▼
Selector
│
▼
Component
This architecture scales significantly better than putting all application logic into components.
41. When Should You Use NgRx?
NgRx is a good choice when your application has:
- Complex shared state
- Multiple features depending on the same data
- Many asynchronous workflows
- Complex state transitions
- Large teams
- Strong debugging requirements
- Event-driven architecture
- Need for predictable state changes
- Significant business logic
Examples:
E-commerce
Banking dashboards
Admin platforms
CRM systems
Large SaaS applications
Enterprise applications
Real-time applications
42. When Should You Avoid NgRx?
For a small application:
5 components
2 API calls
simple forms
little shared state
NgRx may introduce unnecessary complexity.
A simple:
Component
↓
Service
↓
HTTP
architecture might be better.
The goal is not:
"Use NgRx because NgRx is powerful."
The goal is:
"Use the simplest architecture that can reliably manage the application's complexity."
43. The Most Important Mental Model
If you remember only one thing, remember this:
Action = What happened?
Reducer = How does state change?
Effect = What side effect should happen?
Selector = What state does the UI need?
Store = Where is application state centralized?
For example:
User clicks "Add to Cart"
Action:
[Product Page] Add To Cart
↓
Reducer:
Add product to cart state
↓
Effect:
Maybe synchronize cart with backend
↓
Store:
Updated cart
↓
Selector:
Calculate cart count
↓
UI:
Cart: 3 items
That is NgRx.
44. Final Architecture
A mature Angular + NgRx application can look like:
Angular UI
│
▼
Component / Facade
│
▼
Action
│
┌──────────┴──────────┐
│ │
▼ ▼
Reducer Effect
│ │
│ HTTP/API
│ │
│ ▼
│ Success/Failure
│ │
└──────────┬──────────┘
▼
Store
│
▼
Selectors
│
▼
Component / UI
This architecture gives you:
- Predictable state transitions
- Centralized state
- Reactive data flow
- Testable business logic
- Clear separation of concerns
- Better debugging
- Scalable feature architecture
Conclusion
NgRx is not simply a library for storing variables.
It is a state-management architecture built around a very clear philosophy:
Events
↓
Actions
↓
State Transitions
↓
Store
↓
Selectors
↓
UI
Effects extend this architecture by handling asynchronous operations and other side effects.
The real power of NgRx appears when an application becomes complex enough that managing state manually starts becoming difficult.
The key is not to memorize APIs.
Understand the architecture first:
Actions
Reducers
Selectors
Effects
Store
Once those concepts are clear, the NgRx APIs become much easier to learn.
And the most important practical rule is:
Don't introduce NgRx because your application is Angular. Introduce it because your application's state management has become complex enough to justify it.

Top comments (0)