Using NgRx Store in Angular

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

  1. What is State Management
  2. Why Use NgRx Store
  3. Core Concepts of NgRx
  4. Setting Up NgRx in an Angular App
  5. Installing NgRx Store
  6. Understanding Store Structure
  7. Actions Overview
  8. Reducers Overview
  9. Selectors Overview
  10. Dispatching Actions
  11. Selecting State from Store
  12. Example: Simple Counter with NgRx
  13. Step 1: Create Actions
  14. Step 2: Create Reducer
  15. Step 3: Register Store in App Module
  16. Step 4: Use Store in Component
  17. Immutable State Management
  18. Debugging with Redux DevTools
  19. Multiple State Slices
  20. Feature Store Setup
  21. Example: Managing Users State
  22. Creating User Actions
  23. Creating User Reducer
  24. Registering Feature Module
  25. Using Store in User Component
  26. Selectors for Derived State
  27. Combining Multiple Selectors
  28. Using createFeatureSelector and createSelector
  29. Understanding Effects
  30. Why Use NgRx Effects
  31. Example: Fetching Users from API with Effect
  32. Defining the Effect
  33. Registering Effects Module
  34. Dispatching Async Actions
  35. Handling API Success and Failure
  36. Using the Store with HttpClient
  37. Real-World Use Case: Todo App
  38. Folder Structure for NgRx
  39. Defining Actions for Todo
  40. Creating Reducer for Todo
  41. Adding Selectors
  42. Implementing Effects for API Calls
  43. Integrating Todo Store in Components
  44. Best Practices for NgRx Store
  45. Common Mistakes in NgRx Usage
  46. Using Store DevTools for Debugging
  47. Time-Travel Debugging Example
  48. Resetting and Rehydrating State
  49. Using Meta-Reducers
  50. Full Example: Global AppState Interface
  51. Sharing State Between Modules
  52. Memoization in Selectors
  53. StoreModule.forFeature vs StoreModule.forRoot
  54. Handling Nested State
  55. Example: Nested State for Products
  56. Normalizing State Data
  57. Using EntityAdapter for Collections
  58. Example: Managing Product Entities
  59. Handling State Immutably with Spread Operators
  60. Updating and Deleting Data
  61. Lazy Loading State Modules
  62. Combining NgRx with Router Store
  63. Example: Sync Route Parameters with Store
  64. Testing Actions and Reducers
  65. Testing Selectors and Effects
  66. NgRx Store with ComponentStore
  67. NgRx Store vs BehaviorSubject
  68. When Not to Use NgRx
  69. Performance Optimization Techniques
  70. 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:

  1. Actions – Define what happened.
  2. Reducers – Define how state changes.
  3. Selectors – Extract data from state.
  4. 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(() =&gt;
    this.http.get&lt;User&#91;]&gt;('/api/users').pipe(
      map(users =&gt; loadUsersSuccess({ users })),
      catchError(() =&gt; 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 =&gt; 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

  1. Keep reducers pure.
  2. Use Effects for side effects.
  3. Use selectors for derived state.
  4. Avoid direct state mutations.
  5. Keep state normalized.
  6. 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

FeatureNgRx StoreBehaviorSubject
ScopeGlobalLocal
ImmutabilityEnforcedManual
DebuggingRedux DevToolsLimited
Best ForLarge appsSmall 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *