NgRx Store is a state management library for Angular applications, inspired by Redux. It provides a single source of truth for the state of your application, making it easier to manage and debug state changes. In this section, we will cover the basics of NgRx Store, including its core concepts, how to set it up, and how to use it in an Angular application.

Key Concepts

  1. State: The single source of truth for your application's data.
  2. Actions: Plain objects that describe an event or intention to change the state.
  3. Reducers: Pure functions that take the current state and an action, and return a new state.
  4. Selectors: Functions that select a piece of the state.
  5. Effects: Side effects that handle asynchronous operations.

Setting Up NgRx Store

To get started with NgRx Store, you need to install the necessary packages:

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools

Next, you need to set up the store in your Angular application. This involves creating actions, reducers, and integrating the store into your application module.

Creating Actions

Actions are plain objects that describe an event or intention to change the state. They typically have a type property and an optional payload.

// src/app/store/actions/counter.actions.ts
import { createAction, props } from '@ngrx/store';

export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');

Creating Reducers

Reducers are pure functions that take the current state and an action, and return a new state.

// src/app/store/reducers/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from '../actions/counter.actions';

export const initialState = 0;

const _counterReducer = createReducer(
  initialState,
  on(increment, state => state + 1),
  on(decrement, state => state - 1),
  on(reset, state => 0)
);

export function counterReducer(state, action) {
  return _counterReducer(state, action);
}

Integrating the Store

To integrate the store into your Angular application, you need to add the StoreModule to your application module.

// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './store/reducers/counter.reducer';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ count: counterReducer })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Using the Store in Components

To use the store in your components, you need to inject the Store service and select the state you want to use.

// src/app/counter/counter.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset } from '../store/actions/counter.actions';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
      <button (click)="reset()">Reset</button>
      <div>Current Count: {{ count$ | async }}</div>
    </div>
  `
})
export class CounterComponent {
  count$: Observable<number>;

  constructor(private store: Store<{ count: number }>) {
    this.count$ = store.select('count');
  }

  increment() {
    this.store.dispatch(increment());
  }

  decrement() {
    this.store.dispatch(decrement());
  }

  reset() {
    this.store.dispatch(reset());
  }
}

Practical Exercise

Exercise: Implement a Todo List with NgRx Store

  1. Create Actions: Define actions for adding, removing, and toggling todos.
  2. Create Reducers: Implement reducers to handle the defined actions.
  3. Integrate Store: Add the store to your application module.
  4. Use Store in Components: Create a component to display and interact with the todo list.

Solution

Actions

// src/app/store/actions/todo.actions.ts
import { createAction, props } from '@ngrx/store';

export const addTodo = createAction('[Todo] Add Todo', props<{ text: string }>());
export const removeTodo = createAction('[Todo] Remove Todo', props<{ id: number }>());
export const toggleTodo = createAction('[Todo] Toggle Todo', props<{ id: number }>());

Reducers

// src/app/store/reducers/todo.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { addTodo, removeTodo, toggleTodo } from '../actions/todo.actions';

export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export const initialState: Todo[] = [];

const _todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { text }) => [...state, { id: state.length + 1, text, completed: false }]),
  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, action) {
  return _todoReducer(state, action);
}

Integrate Store

// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { todoReducer } from './store/reducers/todo.reducer';

import { AppComponent } from './app.component';
import { TodoComponent } from './todo/todo.component';

@NgModule({
  declarations: [
    AppComponent,
    TodoComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ todos: todoReducer })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Use Store in Components

// src/app/todo/todo.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { addTodo, removeTodo, toggleTodo } from '../store/actions/todo.actions';
import { Todo } from '../store/reducers/todo.reducer';

@Component({
  selector: 'app-todo',
  template: `
    <div>
      <input [(ngModel)]="newTodo" placeholder="New Todo">
      <button (click)="addTodo()">Add Todo</button>
      <ul>
        <li *ngFor="let todo of todos$ | async">
          <input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)">
          {{ todo.text }}
          <button (click)="removeTodo(todo.id)">Remove</button>
        </li>
      </ul>
    </div>
  `
})
export class TodoComponent {
  newTodo: string;
  todos$: Observable<Todo[]>;

  constructor(private store: Store<{ todos: Todo[] }>) {
    this.todos$ = store.select('todos');
  }

  addTodo() {
    if (this.newTodo.trim()) {
      this.store.dispatch(addTodo({ text: this.newTodo }));
      this.newTodo = '';
    }
  }

  removeTodo(id: number) {
    this.store.dispatch(removeTodo({ id }));
  }

  toggleTodo(id: number) {
    this.store.dispatch(toggleTodo({ id }));
  }
}

Summary

In this section, we covered the basics of NgRx Store, including its core concepts, how to set it up, and how to use it in an Angular application. We also provided a practical exercise to implement a todo list with NgRx Store. By mastering these concepts, you will be able to manage the state of your Angular applications more effectively and efficiently.

© Copyright 2024. All rights reserved