Introduction
State management is a critical aspect of building large and scalable Angular applications. As applications grow, managing and synchronizing state across multiple components becomes challenging.
This is where NgRx Store — a reactive state management library built on top of RxJS — provides a robust solution.
The NgRx Store follows the principles of Redux, using a single global state tree, immutable data structures, and unidirectional data flow.
It ensures that your application’s state is predictable, debuggable, and easily testable.
In this comprehensive post, we will explore NgRx Store from the ground up — what it is, how it works, and how to implement it in real-world Angular applications.
Table of Contents
- What is State Management
- Why Use NgRx Store
- Core Concepts of NgRx
- Setting Up NgRx in an Angular App
- Installing NgRx Store
- Understanding Store Structure
- Actions Overview
- Reducers Overview
- Selectors Overview
- Dispatching Actions
- Selecting State from Store
- Example: Simple Counter with NgRx
- Step 1: Create Actions
- Step 2: Create Reducer
- Step 3: Register Store in App Module
- Step 4: Use Store in Component
- Immutable State Management
- Debugging with Redux DevTools
- Multiple State Slices
- Feature Store Setup
- Example: Managing Users State
- Creating User Actions
- Creating User Reducer
- Registering Feature Module
- Using Store in User Component
- Selectors for Derived State
- Combining Multiple Selectors
- Using createFeatureSelector and createSelector
- Understanding Effects
- Why Use NgRx Effects
- Example: Fetching Users from API with Effect
- Defining the Effect
- Registering Effects Module
- Dispatching Async Actions
- Handling API Success and Failure
- Using the Store with HttpClient
- Real-World Use Case: Todo App
- Folder Structure for NgRx
- Defining Actions for Todo
- Creating Reducer for Todo
- Adding Selectors
- Implementing Effects for API Calls
- Integrating Todo Store in Components
- Best Practices for NgRx Store
- Common Mistakes in NgRx Usage
- Using Store DevTools for Debugging
- Time-Travel Debugging Example
- Resetting and Rehydrating State
- Using Meta-Reducers
- Full Example: Global AppState Interface
- Sharing State Between Modules
- Memoization in Selectors
- StoreModule.forFeature vs StoreModule.forRoot
- Handling Nested State
- Example: Nested State for Products
- Normalizing State Data
- Using EntityAdapter for Collections
- Example: Managing Product Entities
- Handling State Immutably with Spread Operators
- Updating and Deleting Data
- Lazy Loading State Modules
- Combining NgRx with Router Store
- Example: Sync Route Parameters with Store
- Testing Actions and Reducers
- Testing Selectors and Effects
- NgRx Store with ComponentStore
- NgRx Store vs BehaviorSubject
- When Not to Use NgRx
- Performance Optimization Techniques
- Summary and Conclusion
1. What is State Management
In Angular, state refers to the data that drives your application. It includes user data, UI flags, authentication tokens, and more.
Without a central system, managing state across multiple components can lead to inconsistency and bugs.
2. Why Use NgRx Store
NgRx provides:
- A single source of truth for your app’s data.
- Predictable state changes through pure functions.
- Easy debugging using Redux DevTools.
- Scalable architecture for complex applications.
3. Core Concepts of NgRx
NgRx Store is built around four main building blocks:
- Actions – Define what happened.
- Reducers – Define how state changes.
- Selectors – Extract data from state.
- Effects – Handle side effects (like API calls).
4. Setting Up NgRx in an Angular App
You can set up NgRx in any Angular project to manage application-wide state.
5. Installing NgRx Store
Use Angular CLI to install NgRx Store and DevTools:
npm install @ngrx/store @ngrx/store-devtools
6. Understanding Store Structure
NgRx Store maintains a global state object known as the state tree.
Each part of your application manages its own slice of state, combined into a single root store.
Example:
interface AppState {
counter: number;
users: UserState;
todos: TodoState;
}
7. Actions Overview
Actions are simple objects that describe an event.
Example:
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
8. Reducers Overview
Reducers are pure functions that handle state changes.
Example:
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, state => state + 1),
on(decrement, state => state - 1),
on(reset, state => 0)
);
9. Selectors Overview
Selectors are functions that extract specific pieces of state.
Example:
export const selectCount = (state: AppState) => state.counter;
10. Dispatching Actions
To change state, dispatch actions using the Store.
this.store.dispatch(increment());
this.store.dispatch(reset());
11. Selecting State from Store
Subscribe to state changes with select()
.
this.store.select('counter').subscribe(count => console.log(count));
12. Example: Simple Counter with NgRx
Let’s build a complete example of a counter using NgRx.
13. Step 1: Create Actions
counter.actions.ts
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
14. Step 2: Create Reducer
counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, state => state + 1),
on(decrement, state => state - 1),
on(reset, state => 0)
);
15. Step 3: Register Store in App Module
app.module.ts
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ count: counterReducer })
],
bootstrap: [AppComponent]
})
export class AppModule {}
16. Step 4: Use Store in Component
app.component.ts
import { Store } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export class AppComponent {
count$ = this.store.select('count');
constructor(private store: Store<{ count: number }>) {}
increment() { this.store.dispatch(increment()); }
decrement() { this.store.dispatch(decrement()); }
reset() { this.store.dispatch(reset()); }
}
17. Immutable State Management
Reducers never mutate the existing state — they always return a new object.
This immutability ensures predictability and time-travel debugging.
18. Debugging with Redux DevTools
Install Redux DevTools extension and add to StoreModule
:
StoreDevtoolsModule.instrument({ maxAge: 25 })
19. Multiple State Slices
You can have multiple reducers managing different parts of state.
StoreModule.forRoot({
counter: counterReducer,
users: userReducer
})
20. Feature Store Setup
Feature stores manage local module state separately using StoreModule.forFeature
.
21. Example: Managing Users State
users.actions.ts
export const loadUsers = createAction('[Users] Load');
export const loadUsersSuccess = createAction('[Users] Load Success', props<{ users: User[] }>());
22. Creating User Reducer
users.reducer.ts
export interface UserState {
users: User[];
loading: boolean;
}
export const initialState: UserState = {
users: [],
loading: false
};
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false }))
);
23. Registering Feature Module
users.module.ts
StoreModule.forFeature('users', userReducer)
24. Using Store in User Component
this.store.dispatch(loadUsers());
this.store.select('users').subscribe(data => console.log(data));
25. Selectors for Derived State
Selectors can derive computed data efficiently.
Example:
export const selectLoadingUsers = (state: AppState) => state.users.loading;
26. Combining Multiple Selectors
export const selectUserNames = createSelector(
selectUsers,
users => users.map(u => u.name)
);
27. Using createFeatureSelector and createSelector
const getUserState = createFeatureSelector<UserState>('users');
export const selectUsers = createSelector(
getUserState,
state => state.users
);
28. Understanding Effects
Effects isolate side effects (like HTTP calls) from components and reducers.
29. Why Use NgRx Effects
Reducers should be pure; Effects allow performing asynchronous operations while keeping reducers clean.
30. Example: Fetching Users from API with Effect
Install Effects:
npm install @ngrx/effects
31. Defining the Effect
users.effects.ts
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
mergeMap(() =>
this.http.get<User[]>('/api/users').pipe(
map(users => loadUsersSuccess({ users })),
catchError(() => EMPTY)
)
)
)
);
constructor(private actions$: Actions, private http: HttpClient) {}
}
32. Registering Effects Module
app.module.ts
EffectsModule.forRoot([UserEffects])
33. Dispatching Async Actions
this.store.dispatch(loadUsers());
34. Handling API Success and Failure
Reducers handle the new state after success or failure actions.
35. Using the Store with HttpClient
NgRx Effects and Store seamlessly integrate with Angular’s HttpClient
for data flow.
36. Real-World Use Case: Todo App
Build a Todo list app using NgRx for managing items, completion, and persistence.
37. Folder Structure for NgRx
src/app/
store/
actions/
reducers/
selectors/
effects/
38. Defining Actions for Todo
todo.actions.ts
export const addTodo = createAction('[Todo] Add', props<{ title: string }>());
export const toggleTodo = createAction('[Todo] Toggle', props<{ id: number }>());
39. Creating Reducer for Todo
export const todoReducer = createReducer(
[],
on(addTodo, (state, { title }) => [...state, { id: Date.now(), title, completed: false }]),
on(toggleTodo, (state, { id }) =>
state.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo)
)
);
40. Adding Selectors
export const selectTodos = (state: AppState) => state.todos;
41. Implementing Effects for API Calls
Add asynchronous saving/loading logic with Effects.
42. Integrating Todo Store in Components
this.store.dispatch(addTodo({ title: 'Learn NgRx' }));
this.store.select('todos').subscribe(console.log);
43. Best Practices for NgRx Store
- Keep reducers pure.
- Use Effects for side effects.
- Use selectors for derived state.
- Avoid direct state mutations.
- Keep state normalized.
- Use feature stores for modular architecture.
44. Common Mistakes in NgRx Usage
- Mutating state directly.
- Overusing Store for simple local component data.
- Forgetting to handle errors in Effects.
- Ignoring unsubscribe patterns.
45. Using Store DevTools for Debugging
Add in module imports:
StoreDevtoolsModule.instrument({ maxAge: 50 })
Then use Redux DevTools in the browser for time-travel debugging.
46. Time-Travel Debugging Example
You can navigate between past actions to see the app state evolution.
47. Resetting and Rehydrating State
Reset entire state via an action:
on(resetAppState, () => initialState)
48. Using Meta-Reducers
Meta-reducers wrap around normal reducers for logging, persistence, etc.
Example:
export function logger(reducer) {
return function(state, action) {
console.log('State before:', state);
console.log('Action:', action);
return reducer(state, action);
};
}
49. Full Example: Global AppState Interface
interface AppState {
counter: number;
users: UserState;
todos: TodoState;
}
50. Sharing State Between Modules
You can inject Store into any module, component, or service and share state globally.
51. Memoization in Selectors
Selectors are memoized, meaning they cache results for performance optimization.
52. StoreModule.forFeature vs StoreModule.forRoot
forRoot()
→ defines root store.forFeature()
→ defines feature-specific store slices.
53. Handling Nested State
Use nested interfaces and selectors to handle deeply structured state.
54. Example: Nested State for Products
interface ProductState {
categories: string[];
items: Product[];
}
55. Normalizing State Data
Normalize data to store entities as key-value pairs instead of nested arrays.
56. Using EntityAdapter for Collections
export const adapter = createEntityAdapter<Product>();
57. Example: Managing Product Entities
export const productsReducer = createReducer(
adapter.getInitialState(),
on(addProduct, (state, { product }) => adapter.addOne(product, state))
);
58. Handling State Immutably with Spread Operators
Always use spread syntax (...
) for immutability in reducers.
59. Updating and Deleting Data
on(updateTodo, (state, { id, changes }) =>
state.map(todo => todo.id === id ? { ...todo, ...changes } : todo)
);
60. Lazy Loading State Modules
You can lazy-load feature stores with routing for large applications.
61. Combining NgRx with Router Store
Sync route parameters and navigation state with Store.
StoreRouterConnectingModule.forRoot()
62. Example: Sync Route Parameters with Store
Access route data via selectors.
63. Testing Actions and Reducers
Write unit tests to ensure reducers return correct state transitions.
64. Testing Selectors and Effects
Mock store and test computed selector logic separately for reliability.
65. NgRx Store with ComponentStore
Use ComponentStore
for local reactive state when full NgRx Store is unnecessary.
66. NgRx Store vs BehaviorSubject
Feature | NgRx Store | BehaviorSubject |
---|---|---|
Scope | Global | Local |
Immutability | Enforced | Manual |
Debugging | Redux DevTools | Limited |
Best For | Large apps | Small isolated state |
67. When Not to Use NgRx
Avoid using NgRx for simple or local-only states that don’t need persistence or history tracking.
68. Performance Optimization Techniques
- Use OnPush change detection.
- Split large stores into feature modules.
- Memoize selectors properly.
Leave a Reply