DEV Community

Cover image for Step-By-Step Guide for integrating NgRx State Management with Angular 16!
Code Craft-Fun with Javascript
Code Craft-Fun with Javascript

Posted on • Edited on

Step-By-Step Guide for integrating NgRx State Management with Angular 16!

NGRX is a popular library for state management in Angular applications. It helps to manage the state of an application in a predictable and scalable way. In this guide, we will go through the step-by-step process of implementing NGRX by building a small Todo application.

You can refer to the full code here

Step 1: Install NGRX

To install NGRX, run the following command in your terminal:

npm install @ngrx/store @ngrx/effects --save
Enter fullscreen mode Exit fullscreen mode

The @ngrx/store package provides the core state management functionality for NGRX. The @ngrx/effects package provides a way to handle side-effects in your application.

Our application folder structure will look like this-

src/
   | store/
     | actions.ts
     | reducers.ts
     | selectors.ts
     | store.ts
     | todo.model.ts 
   | main.ts
   | todo.component.ts
   | todo.component.html
Enter fullscreen mode Exit fullscreen mode

Step 2: Define the Model Todo

The first step is to define the model of your data in store/todo.model.ts -

export interface Todo {
  id: number | string;
  description: string;
  completed: boolean;
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define the Todo interface with some properties.

Step 3: Define the Service and Actions

Service will contain our todo api service with the getAll function. This will act as a fake backend service. We will be going to use the Observable and imitate the latency to show the loader.
Define the todo service in store/service.ts

@Injectable()
export class ToDoService {
 // fake backend
  getAll(): Observable<Todo[]> {
    return of(
      [{
        id: 1,
        description: 'description 1',
        completed: false
      },
      {
        id: 2,
        description: 'description 2',
        completed: false
      }]
    ).pipe(delay(2000))
  }
}
Enter fullscreen mode Exit fullscreen mode

Actions are messages that describe a state change in your application. They are plain JavaScript objects that have a type property and an optional payload. Define the actions in store/actions.ts

export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction('[Todo] Load Todos Success', props<{ todos: Todo[] }>());
export const loadTodosFailure = createAction('[Todo] Load Todos Failure', props<{ error: string }>());
export const addTodo = createAction('[Todo] Add Todo', props<{ todo: Todo }>());
export const updateTodo = createAction('[Todo] Update Todo', props<{ todo: Todo }>());
export const deleteTodo = createAction('[Todo] Delete Todo', props<{ id: string }>());
Enter fullscreen mode Exit fullscreen mode

In this example, we define several actions for managing the Todo state. The loadTodos action is used to load the list of todos from the server. The loadTodosSuccess action is dispatched when the todos are loaded successfully. The loadTodosFailure action is dispatched when there is an error loading the todos. The addTodo, updateTodo, and deleteTodo actions are used to add, update, and delete todos respectively.

Step 4: Define the Reducers

Reducers are pure functions that take the current state and an action and return a new state. They are responsible for handling the state changes in your application. Define the actions in store/reducers.ts

export interface TodoState {
todos: Todo[];
loading: boolean;
error: string;
}
export const initialState: TodoState = {
todos: [],
loading: false,
error: ''
};
export const todoReducer = createReducer(
initialState,

on(TodoActions.loadTodos, state => ({ ...state, loading: true })),

on(TodoActions.loadTodosSuccess, (state, { todos }) =>({ ...state, todos, loading: false })),

on(TodoActions.loadTodosFailure, (state, { error }) => ({ ...state, error, loading: false })),

on(TodoActions.addTodo, (state, { todo }) => ({ ...state, todos: [...state.todos, todo] })),

on(TodoActions.updateTodo, (state, { todo }) => ({ ...state, todos: state.todos.map(t => t.id === todo.id ? todo : t) })),

on(TodoActions.deleteTodo, (state, { id }) => ({ ...state, todos: state.todos.filter(t => t.id !== id) })),
);
Enter fullscreen mode Exit fullscreen mode

In this example, we define a reducer for managing the Todo state. The todoReducer function takes the initialState and a set of reducer functions defined using the on function from @ngrx/store. Each reducer function handles a specific action and returns a new state.

Step 5: Define the Effects

Effects are services that listen for actions and perform side-effects such as HTTP requests or interacting with the browser's APIs.


@Injectable()
export class TodoEffects {

  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TodoActions.loadTodos),
      mergeMap(() =>
        this.todoService.getAll().pipe(
          map((todos) => TodoActions.loadTodosSuccess({ todos })),
          catchError((error) =>
            of(TodoActions.loadTodosFailure({ error: error.message }))
          )
        )
      )
    )
  );
  constructor(private actions$: Actions, private todoService: ToDoService) {}
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define an effect for handling the loadTodos action. The loadTodos$ effect listens for the loadTodos action using the ofType operator from @ngrx/effects. When the action is dispatched, the effect calls the getAll method from the TodoService and dispatches either the loadTodosSuccess or loadTodosFailure action depending on the result.

Step 6: Define interface for the Store

This will be defined in the store/sotre.ts file -

export interface AppState {
  todo: TodoState
}

export interface AppStore {
  todo: ActionReducer<TodoState, Action>;
}

export const appStore: AppStore = {
  todo: todoReducer
}

export const appEffects = [TodoEffects];
Enter fullscreen mode Exit fullscreen mode

AppState interface will define all the feature properties of the application. Here we have a single feature called as todo which is of type TodoState. We can have multiple features inside our application like this.

AppStore interface will define all the reducer types used in our app. In this case we have a single todo reducer so we will map the todoReducer to the todo property. appStore will be used to config our Store Module.

appEffects will have the array of defined effects classes. This will be used to register the effects in the application.

Step 7: Register the Store and Effects in the main standalone component

To use the NGRX store in your application, you need to register the StoreModule in your AppModule. We will be using a standalone components here -

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [
    CommonModule
  ],
  template: ``,
})
export class App {
}

bootstrapApplication(App, {
  // register the store providers here
  providers: [
    provideStore(appStore),
    provideEffects(appEffects),
    ToDoService
  ]
});
Enter fullscreen mode Exit fullscreen mode

In this example, we register the store using the appStore. We also register the Effects with appEffects.

Step 8: Use the Store in ToDo List Component

To use the store in your components, you need to inject the Store service and dispatch actions.We will create a standalone TodoListComponent in src/todo-list.component.ts with the html file in src/todo-list.component.html

@Component({
standalone: true,
selector: 'app-todo-list',
imports: [NgFor, NgIf, AsyncPipe, JsonPipe],
templateUrl: './todo-list.component.html'
})
export class TodoListComponent {
todos$: Observable<Todo[]>;
isLoading$: Observable<boolean>;

constructor(private store: Store<AppState>) {
  this.todos$ = this.store.select(todoSelector);
  this.isLoading$ = this.store.select(state => state.todo.loading);
  this.loadTodos();
}

loadTodos() {
this.store.dispatch(TodoActions.loadTodos());
}

addTodo(index: number) {

const todo: Todo = {id: index, description: 'New Todo', completed: false };
this.store.dispatch(TodoActions.addTodo({ todo }));
}

complete(todo: Todo) {
  this.store.dispatch(TodoActions.updateTodo({todo : {...todo, completed: true}}));
}
}
Enter fullscreen mode Exit fullscreen mode

todo-list.component.html

<ng-container *ngIf="todos$ | async as todos;">

  <ul *ngIf="todos.length; else empty">
    <li *ngFor="let todo of todos$ | async; let i = index;">
    {{todo.id}}  {{ todo.description }} - {{ todo.completed ? 'Done' : 'Pending' }}
      <button [disabled]="todo.completed" (click)="complete(todo)">
      complete
    </button>
    </li>

  </ul>

  <ng-template #empty>
    <div>No Data</div>
  </ng-template>


<button (click)="loadTodos()">Refresh</button>
<button (click)="addTodo(todos.length + 1)">Add</button>

<div *ngIf="isLoading$ | async">Loading...</div>

</ng-container>
Enter fullscreen mode Exit fullscreen mode

In this example, we define a component that uses the Store service to load and display todos. We inject the Store service and select the todos property from the state using the select method. We also define three methods for dispatching the loadTodos, addTodo and updateTodo actions. The async pipe is used to subscribe to the todos$ observable and display the list of todos.

Step 9: Register the ToDo List Component in main component

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [
    CommonModule,
    TodoListComponent
  ],
  template: `
    <app-todo-list/>
  `,
})
export class App {
}
Enter fullscreen mode Exit fullscreen mode

Add TodoListComponent in the imports array. Add the <app-todo-list/> tag to display the TodoListComponent.

Conclusion

In this guide, we have gone through the step-by-step process of implementing NGRX in an Angular application. We have seen how to define the state, actions, reducers, effects, and how to use the store in components. By following these steps, you can easily manage the state of your application in a scalable and predictable way using NGRX.

Top comments (4)

Collapse
 
zviad_sharadze_2a83189033 profile image
Zviad Sharadze

Thanks for article.
In step 8 there's error in code:
In todo-list.component.ts
instead of:
this.todos$ = this.store.select(todoSelector);
should be:
this.todos$ = this.store.select((state) => state.todo.todos);

Collapse
 
marko_askovic_7d77f4f6626 profile image
Marko Askovic • Edited

The line :

import * as TodoActions from './actions';

should appear (for better understanding).

Collapse
 
shojchi profile image
Roman Hordiichuk • Edited

Great article! Thanks for your efforts. I found one typo after 6 chapter header:
This will be defined in the store/sotre.ts file -

Collapse
 
alishan021 profile image
alishan021

Thanks, Finally got a good explanation.