skip to content
Mohammad Rebati

Effective State Management with NgRx in Angular

/ 7 min read

NgRx in Angular

My Adventure with State Management: From Chaos to Control with NgRx

Hey there, fellow developers! Grab your favorite beverage, settle in, and let me take you on a storytelling journey about how I transformed my Angular project from a tangled web of state mishaps into a streamlined, efficient application using NgRx. Spoiler alert: there were laughs, some facepalms, and a whole lot of learning.

The Problem: State Management Woes in Angular

It was a bright Monday morning (okay, maybe a bit before coffee), and I was knee-deep in developing a feature-rich Angular application. As the app grew, so did the complexity of managing its state. Components were passing data back and forth like a game of hot potato, and the once manageable service-based state was turning into a maintenance nightmare.

Symptoms:

  • Data Inconsistency: Different components had varying states of the same data.
  • Prop Drilling Hell: Passing data through multiple layers became tedious.
  • Unpredictable Behavior: Bug hunting was akin to finding a needle in a haystack.
  • Scalability Issues: Adding new features felt like adding more branches to an already sprawling tree.

I knew something had to change. Enter NgRx, my knight in shining armor for state management in Angular.

State Management

Introducing NgRx: The Hero of Our Story

NgRx is a powerful state management library inspired by Redux, tailored for Angular applications. It leverages RxJS to provide a reactive state management solution, making your application’s state predictable and easier to debug.

Key Concepts of NgRx:

  • Store: A single source of truth for your application’s state.
  • Actions: Events that describe something that happened in the application.
  • Reducers: Pure functions that handle state transitions based on actions.
  • Selectors: Functions to query specific pieces of state.
  • Effects: Side effects management, handling asynchronous operations like API calls.

Setting the Stage: Our Project Setup

Let me walk you through how I integrated NgRx into my Angular project. We’ll create a simple to-do application to keep things practical and engaging.

Step 1: Installing NgRx

First things first, let’s add NgRx to our Angular project.

Terminal window
ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/store-devtools@latest
ng add @ngrx/router-store@latest

Step 2: Defining the State

We start by defining the state structure for our to-do app.

src/app/state/todo.state.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}
export interface AppState {
todos: Todo[];
}

Step 3: Creating Actions

Actions represent the events that can occur in our application.

src/app/state/todo.actions.ts
import { createAction, props } from '@ngrx/store';
import { Todo } from './todo.state';
export const addTodo = createAction(
'[Todo] Add Todo',
props<{ todo: Todo }>()
);
export const removeTodo = createAction(
'[Todo] Remove Todo',
props<{ id: number }>()
);
export const toggleTodo = createAction(
'[Todo] Toggle Todo',
props<{ id: number }>()
);

Step 4: Building Reducers

Reducers handle the state transitions based on the actions dispatched.

src/app/state/todo.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { addTodo, removeTodo, toggleTodo } from './todo.actions';
import { Todo } from './todo.state';
export const initialState: Todo[] = [];
const _todoReducer = createReducer(
initialState,
on(addTodo, (state, { todo }) => [...state, todo]),
on(removeTodo, (state, { id }) => state.filter(todo => todo.id !== id)),
on(toggleTodo, (state, { id }) =>
state.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
);
export function todoReducer(state: any, action: any) {
return _todoReducer(state, action);
}

Step 5: Setting Up Selectors

Selectors help us retrieve specific pieces of state efficiently.

src/app/state/todo.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { AppState, Todo } from './todo.state';
export const selectTodos = createFeatureSelector<AppState, Todo[]>('todos');
export const selectCompletedTodos = createSelector(
selectTodos,
(todos: Todo[]) => todos.filter(todo => todo.completed)
);
export const selectIncompleteTodos = createSelector(
selectTodos,
(todos: Todo[]) => todos.filter(todo => !todo.completed)
);

Step 6: Integrating Store Module

Finally, we integrate the store into our Angular module.

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { todoReducer } from './state/todo.reducer';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ todos: todoReducer }),
StoreDevtoolsModule.instrument({ maxAge: 25 }),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

Implementing the To-Do Feature: From Chaos to Control

With NgRx set up, let’s implement the to-do feature step-by-step.

Adding a To-Do

src/app/components/add-todo/add-todo.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { addTodo } from '../../state/todo.actions';
import { Todo } from '../../state/todo.state';
@Component({
selector: 'app-add-todo',
template: `
<input [(ngModel)]="title" placeholder="New To-Do" />
<button (click)="add()">Add</button>
`,
})
export class AddTodoComponent {
title: string = '';
constructor(private store: Store) {}
add() {
if (this.title.trim()) {
const newTodo: Todo = {
id: Date.now(),
title: this.title,
completed: false,
};
this.store.dispatch(addTodo({ todo: newTodo }));
this.title = '';
}
}
}

Displaying To-Dos

src/app/components/todo-list/todo-list.component.ts
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Todo } from '../../state/todo.state';
import { selectTodos } from '../../state/todo.selectors';
import { toggleTodo, removeTodo } from '../../state/todo.actions';
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let todo of todos$ | async">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggle(todo.id)"
/>
<span [ngClass]="{ completed: todo.completed }">{{ todo.title }}</span>
<button (click)="remove(todo.id)">Remove</button>
</li>
</ul>
`,
styles: [
`
.completed {
text-decoration: line-through;
}
`,
],
})
export class TodoListComponent {
todos$: Observable<Todo[]>;
constructor(private store: Store) {
this.todos$ = this.store.pipe(select(selectTodos));
}
toggle(id: number) {
this.store.dispatch(toggleTodo({ id }));
}
remove(id: number) {
this.store.dispatch(removeTodo({ id }));
}
}

The Ups and Downs: Navigating NgRx Terrain

High Points

  1. Predictable State: With a single source of truth, tracking state changes became straightforward.
  2. Enhanced Debugging: NgRx Store DevTools allowed me to time-travel debug and inspect actions effortlessly.
  3. Scalability: Adding new features didn’t feel like tiptoeing through a minefield. The architecture supported growth gracefully.
Debugging

Challenges Faced

  1. Boilerplate Code: Initially, the amount of code felt overwhelming. Defining actions, reducers, and selectors for even simple tasks was tedious.
  2. Learning Curve: Grasping the reactive programming paradigm with RxJS took some time. Understanding how actions flow through reducers and effects wasn’t instantaneous.
  3. Overkill for Simple Apps: For smaller projects, NgRx might introduce unnecessary complexity.

Avoiding the Villains: Memory Leaks and Performance Pitfalls

While NgRx is robust, it’s not immune to common pitfalls, especially around memory management.

Potential Memory Leaks

  • Unsubscribed Observables: Forgetting to unsubscribe from store selectors or effects can lead to memory leaks.

    Solution: Utilize the async pipe in templates or manage subscriptions with takeUntil in components.

  • Improper Effect Cleanup: Effects that listen to streams without proper termination logic can cause leaks.

    Solution: Ensure that effects complete their streams appropriately and avoid infinite subscriptions unless necessary.

Performance Considerations

  • Selector Overuse: Overly granular selectors can lead to unnecessary recalculations and re-renders.

    Solution: Structure selectors to minimize recalculations and use memoization effectively.

  • Large State Trees: Managing excessively large state trees can slow down the application.

    Solution: Normalize the state and lazy-load state slices as needed.

Wrapping Up: The Transformation

Integrating NgRx into my Angular project was like upgrading from a cluttered workshop to a well-organized studio. Sure, the initial setup was a bit of a hurdle, and the boilerplate code made me question my life choices a few times, but the benefits far outweighed the downsides.

With NgRx:

  • State management became predictable and maintainable.
  • Debugging transformed from a headache to a breeze.
  • The application’s scalability potential skyrocketed.

Final Thoughts and Recommendations

If you’re grappling with state management in your Angular projects, NgRx is a worthy contender. Embrace the learning curve, streamline your state with actions and reducers, and leverage the power of RxJS for reactive state flows. Just remember to stay vigilant against memory leaks and performance hiccups by following best practices.

Success

Happy coding, and may your states always be managed with grace!