Step-By-Step Guide for NgRx with Angular 16!

Step-By-Step Guide for NgRx with Angular 16!

Todo App using Angular and NgRx

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

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

Step 2: Define the Model for 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;
}

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))
  }
}

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 }>());

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) })),
);

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) {}
}

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 dispatches either the loadTodosSuccess or loadTodosFailure action depends on the result.

Step 6: Define the 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];

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 an 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 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
  ]
});

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}}));
}
}

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 the main component

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

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.

Did you find this article valuable?

Support Code Craft - Fun With Javascript by becoming a sponsor. Any amount is appreciated!