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
- State: The single source of truth for your application's data.
- Actions: Plain objects that describe an event or intention to change the state.
- Reducers: Pure functions that take the current state and an action, and return a new state.
- Selectors: Functions that select a piece of the state.
- Effects: Side effects that handle asynchronous operations.
Setting Up NgRx Store
To get started with NgRx Store, you need to install the necessary packages:
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
- Create Actions: Define actions for adding, removing, and toggling todos.
- Create Reducers: Implement reducers to handle the defined actions.
- Integrate Store: Add the store to your application module.
- 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.
Angular 2+ Course
Module 1: Introduction to Angular
Module 2: TypeScript Basics
- Introduction to TypeScript
- TypeScript Variables and Data Types
- Functions and Arrow Functions
- Classes and Interfaces