Managing State in Angular Apps with NgRx - Part 2: Using HTTP and Effects

Managing State in Angular Apps with NgRx - Part 2: Using HTTP and Effects

Introduction: In the first part of this blog series, we explored how to manage state in Angular applications using NgRx Store and discussed the basics of reducers, actions, and selectors. We also set up a basic counter example to illustrate state management concepts. In this continuation, we will extend the example to demonstrate how to fetch data from an HTTP API using NgRx Effects.

Prerequisites: To get the most out of this blog post, you should have a basic understanding of Angular, RxJS, and NgRx Store. If you haven’t read the first part of the series, I highly recommend doing so to familiarize yourself with the core concepts of NgRx Store.

Let’s get started!

Step 1: Create a Model for the Data To begin, we need a model to represent the data fetched from the API. For this example, we’ll create a simple Post model with id, title, and body properties.

models/post.model.ts

export interface Post {
  id: number;
  title: string;
  body: string;
}

Step 2: Define Actions for HTTP Request Next, we’ll create actions to handle the HTTP request for fetching posts from the API. Create a new file called post-actions.ts and define the actions as follows:

actions/post-actions.ts

import { createAction, props } from '@ngrx/store';
import { Post } from '../models/post.model';

export const loadPosts = createAction('[Post] Load Posts');
export const loadPostsSuccess = createAction('[Post] Load Posts Success', props<{ posts: Post[] }>());
export const loadPostsFailure = createAction('[Post] Load Posts Failure', props<{ error: any }>());

Step 3: Implement an Effect to Handle HTTP Request Now, we need an effect to handle the loadPosts action and perform the actual HTTP request. Create a file called post-effects.ts and define the effect as follows:

effects/post-effects.ts

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { mergeMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { PostService } from '../services/post.service';
import { loadPosts, loadPostsSuccess, loadPostsFailure } from '../actions/post-actions';

@Injectable()
export class PostEffects {
  loadPosts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadPosts),
      mergeMap(() =>
        this.postService.getPosts().pipe(
          map((posts) => loadPostsSuccess({ posts })),
          catchError((error) => of(loadPostsFailure({ error })))
        )
      )
    )
  );

  constructor(private actions$: Actions, private postService: PostService) {}
}
(ads)

Step 4: Implement a Service for HTTP Request Create a service to handle the actual HTTP request for loading posts from the API. For this example, we’ll use Angular’s built-in HttpClient module. Create a file called post.service.ts and define the service as follows:

services/post.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post } from '../models/post.model';

@Injectable({
  providedIn: 'root',
})
export class PostService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/posts';

  constructor(private http: HttpClient) {}

  getPosts(): Observable<Post[]> {
    return this.http.get<Post[]>(this.apiUrl);
  }
}

Step 5: Update the Reducer to Handle the Action Next, we need to update the postReducer to handle the loadPostsSuccess action and update the state with the fetched posts. We’ll also handle the loadPostsFailure action to clear the posts array and store the error, if any.

reducer/post.reducer.ts

import { createReducer, on } from '@ngrx/store';
import { loadPostsSuccess, loadPostsFailure } from '../actions/post-actions';
import { Post } from '../models/post.model';

export interface PostState {
  posts: Post[];
}

export const initialState: PostState = {
  posts: [],
};

const _postReducer = createReducer(
  initialState,
  on(loadPostsSuccess, (state, { posts }) => ({ ...state, posts: [...state.posts, ...posts] })),
  on(loadPostsFailure, (state, { error }) => ({ ...state, posts: [], error }))
);

export function postReducer(state: AppState | undefined, action: any) {
  return _postReducer(state, action);
}

Step 6: Dispatch the Action from a Component Finally, we’ll update the component that needs to load the posts to dispatch the loadPosts action. In this example, let’s assume the component is named PostListComponent. Open the file containing PostListComponent and make the following changes:

post-list.component.ts

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { loadPosts } from '../actions/post-actions';
import { Post } from '../models/post.model';
import { AppState } from '../reducer/counter.reducer';

@Component({
  selector: 'app-post-list',
  template: `
    <button (click)="onLoadPosts()">Load Posts</button>
    <ul>
      <li *ngFor="let post of posts$ | async">
        {{ post.title }}
      </li>
    </ul>
  `,
})
export class PostListComponent {
  posts$: Observable<Post[]>;

  constructor(private store: Store<PostState>) {}

  onLoadPosts(): void {
    this.store.dispatch(loadPosts());
  }
}

Conclusion:

In this blog post, we extended the counter example from Part 1 to demonstrate how to use NgRx Effects for handling HTTP requests and fetching data from an API. We defined actions for loading posts, implemented an effect to handle the HTTP request, and created a service to interact with the API. Finally, we updated the reducer to handle the success and failure actions and demonstrated how to dispatch the action from a component.

With NgRx and NgRx Effects, you can efficiently manage state and handle asynchronous operations in your Angular applications. The separation of concerns provided by this pattern helps to keep your codebase organized and maintainable.

Next Post Previous Post
No Comment
Add Comment
comment url