← Back to list

Go Pro with RxJS

Master Observables for Efficient Code

TechieThreads · 2024-12-09 18:44 · 3 claps · 5.7 min read
#rxjs #angular #reactive-programming #switchmap #nested-subscriptions
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development

Part 1

Go Pro with RxJS

Master Observables for Efficient Code

Reactive programming with RxJS often involves patterns that can benefit from thoughtful refactoring and optimization. In this article, we’ll explore some common mistakes developers make and demonstrate how to improve them using RxJS operators for enhanced maintainability, readability, and performance. During my code reviews, I frequently encounter certain bad practices, and I’ll be highlighting those scenarios here. This will be the first in a series of articles where I share insights on writing better RxJS code.

1. Inefficient Use of map for Side Effects

export class MockApiService {
  constructor() {}

  getEmployees(): Observable<Employee[]> {
    // Assume we are getting the list of employees from a API call
    const employees: Employee[] = [
      { name: 'Mohammed', age: 45, id: '1' },
      { name: 'Asif', age: 45, id: '2' },
    ];
    return of(employees);
  }

  getEvaluationDetails(employeeIds: string[]): Observable<EvaluationDetails[]> {
    // assume we are passing employee Ids and performing a API call
    const evaluationDetails: EvaluationDetails[] = [
      { employeeId: '1', score: 2 },
      { employeeId: '12', score: 22 },
    ];
    return of(evaluationDetails);
  }
}

export interface Employee { 
    name:string;
    age:number;
    id:string;
    department?: string;
}

export interface EvaluationDetails {
    employeeId: string;
    score:number;
}

Here’s my service implementation where I’m working with two API calls. The first, getEmployees, retrieves a list of employees, while the second, getEvaluationDetails, fetches the evaluation scores for specific employee IDs passed as parameters. While the code doesn't explicitly show the use of employee IDs in the API call, you can assume they are being utilized appropriately.

  public ngOnInit(): void {
    this.mockService
      .getEmployees()
      .pipe(
        switchMap((employees) => {
        // This is a mistake in this code.
        // Side effects can be performed seperately
          const userIds: string[] = [];
          employees.map((employee) => {
            if (employee.department === 'ECE') {
              userIds.push(employee.department);
            }
          });
          return this.mockService.getEvaluationDetails(userIds);
        })
      )
      .subscribe((employeeWithScores: EvaluationDetails[]) => {
        console.log(employeeWithScores);
      });
  }

Let’s fix this! The solution is straightforward: we’ll use the map operator to extract the department value and the filter operator to select only the entries with the department as 'ECE'.

// Perform the side effects using map operator and then pass the values to switchmap
public ngOnInit(): void {
    this.mockService
      .getEmployees()
      .pipe(
        map((employees) =>
          employees
            .filter(
              (employee) => employee.department === 'ECE' && employee.department
            )
            .map((employee) => employee.department as string)
        ),
        switchMap((userIds: string[]) =>
          this.mockService.getEvaluationDetails(userIds)
        )
      )
      .subscribe((employeeWithScores: EvaluationDetails[]) => {
        console.log(employeeWithScores);
      });
  }

If you want to avoid multiple loops and combine the operations into a single iteration, you can use the reduce operator.

// Correct way
public ngOnInit(): void {
    this.mockService
      .getEmployees()
      .pipe(
        map((employees) =>
          employees.reduce((userIds: string[], employee) => {
            if (employee.department === 'ECE') {
              userIds.push(employee.department);
            }
            return userIds;
          }, [])
        ),
        switchMap((userIds: string[]) =>
          this.mockService.getEvaluationDetails(userIds)
        )
      )
      .subscribe((employeeWithScores: EvaluationDetails[]) => {
        console.log(employeeWithScores);
      });
  }

2. Use Finalize to stop loading

One of the most common mistakes developers make is mishandling the visibility of loaders on a page. I’ve often come across code where the loader is either never hidden or is turned off at the wrong time, leading to poor user experiences. This happens because we forget to set the loader’s visibility to false once an operation is complete, or we place the logic in an incorrect spot, such as prematurely within a stream or before an asynchronous operation concludes.

In our project, we use NGXS for state management. Whenever an API is called, we manage the loading state through NGXS actions. However, I often notice developers handling loaders directly in the action and unsubscribing within the catchError operator, which I find problematic. This approach leads to duplicated logic, as they also need to handle the loader state in other places, such as after subscribing or in additional catchError blocks. There’s a better way to manage this consistently and efficiently.

A more reliable approach is to use the finalize operator in RxJS. By placing the logic to hide the loader within finalize, you ensure that it is executed regardless of whether the observable stream completes successfully or with an error. This guarantees a consistent cleanup mechanism for managing loaders, keeping the user experience smooth and bug-free. **finalize is executed when the observable completes or errors out.**


@Action(FetchData)
  public fetchData(ctx: StateContext<AppStateModel>) {
    ctx.dispatch(new SetLoading(true)); 

    return this.apiService.getData().pipe(
      tap((data) => {
        ctx.patchState({ data }); 
      }),
      catchError((error) => {
        // Instead of making it false here. use finalize
        return of([]);
      }),
      finalize(() => {
        ctx.dispatch(new SetLoading(false)); // Use this way to set the loader to false
      })
    );
  }

@Action(SetLoading)
  setLoading(ctx: StateContext<AppStateModel>, action: SetLoading) {
    ctx.patchState({ isLoading: action.isLoading });
  }

3. Get comfortable with forkJoin and switchMap

I need to implement a scenario where a user’s email is passed to an API to retrieve the corresponding user ID, followed by multiple API calls to fetch permissions. Based on these permissions, the button disable logic is determined.

I encourage you to code this scenario yourself. Use the getUserIdByEmail API to fetch the userId, then pass this userId to the getPermission1 and getPermission2 API calls to retrieve permissions. Based on the permissions, implement the logic to disable the buttons.

// Assume these are the API calls.

 getUserIdByEmail(email: string): Observable<{ userId: string }> {
  // Assume we pass this email to fetch the data from API call.
  // Same applies to the other API calls
    return of({ userId: '12345' }); 
  }

  getPermission1(userId: string): Observable<boolean> {
    return of(true); 
  }

  getPermission2(userId: string): Observable<boolean> {
    return of(false);
  }
public ngOnInit(): void {
    this.fetchPermissionsAndDisableButton('mohammed@gmail.com');
  }

  private fetchPermissionsAndDisableButton(email: string): void {
    this.mockService
      .getUserIdByEmail(email)
      .pipe(
        switchMap((user) =>
          combineLatest({  // This would work but I wouldn't recommend this
            permission1: this.mockService.getPermission1(user.userId),
            permission2: this.mockService.getPermission2(user.userId),
          })
        ),
        map(({ permission1, permission2 }) => {
          const disableButton1 = !permission1;
          const disableButton2 = !permission2;
          return { disableButton1, disableButton2 };
        })
      )
      .subscribe(({ disableButton1, disableButton2 }) => {
        console.log('Disable Button 1:', disableButton1);
        console.log('Disable Button 2:', disableButton2);
      });
  }

// Best way to do it

  private fetchPermissionsAndDisableButton(email: string): void {
    this.mockService
      .getUserIdByEmail(email)
      .pipe(
        switchMap((user) =>
          forkJoin({ // Combine multiple API calls
            permission1: this.mockService.getPermission1(user.userId),
            permission2: this.mockService.getPermission2(user.userId),
          })
        ),
        map(({ permission1, permission2 }) => {
          const disableButton1 = !permission1;
          const disableButton2 = !permission2;
          return { disableButton1, disableButton2 };
        })
      )
      .subscribe(({ disableButton1, disableButton2 }) => {
        console.log('Disable Button 1:', disableButton1);
        console.log('Disable Button 2:', disableButton2);

I’ve seen developers use combineLatest for this type of scenario, and while it might work, it’s not the ideal choice here. I wouldn’t recommend using combineLatest in this case. In this case getPermission1and getPermission2wont emit values multiple times so there is no point to use combineLatest.

4. Handing nested subscriptions in the right way

It is considered bad practice to write nested subscriptions, as they negatively impact the readability and maintainability of your code. Avoid using nested subscriptions whenever possible.

Lets say we have a scenario, we need to pass an email to retrieve the profile data, then extract the userId from the profile to fetch the list of posts. Afterward, we use the firstPostId to fetch the comments for the first post. Basically what I am trying to say is we have nested subscriptions.

 private fetchUserData(userEmail: string): void {
    this.userService.getUserProfile(userEmail).subscribe((profile) => {
      this.postService.getUserPosts(profile.userId).subscribe((posts) => {
        if (posts.length > 0) {
          const firstPostId = posts[0].id;
          this.commentService.getPostComments(firstPostId).subscribe((comments) => {
           this.comments = comments
          });
        } 
      });
    });
  }

Always use a single subscription to keep the code clean, maintainable, and more readable.

// Right way to code 
private fetchUserData(userEmail: string): void {
    this.userService.getUserProfile(userEmail).pipe(
      switchMap((profile) => { 
// always use switch map to write code for this scenario
        return this.postService.getUserPosts(profile.userId);
      }),
      switchMap((posts) => {
        if (posts.length > 0) {
          const firstPostId = posts[0].id;
          return this.commentService.getPostComments(firstPostId);
        } else {
          return of([]);
        }
      })
    ).subscribe((comments) => {
      this.comments = comments;
    });
  }

In this article, we explored best practices for handling side effects and managing observables in Angular development. First, we discuss the inefficiency of using map for side effects and recommend alternatives that avoid altering the observable stream. We also emphasize the importance of using finalize to properly manage loading states and ensure clean-up after operations complete. Additionally, we highlight the benefits of leveraging operators like forkJoin and switchMap to simplify handling multiple observables and chaining complex logic. Finally, we address the issue of nested subscriptions, explaining why they should be avoided for cleaner, more readable code, and provide guidance on handling subscriptions more effectively. By applying these practices, you can improve both the performance and maintainability of your code.

I will be writing more articles that focus on RxJS, providing insights and best practices for working with reactive programming. Stay tuned for further updates. I will be reviewing more code in my organization and if I come across ay bad practices or any good practices I will share them in my next articles. If you have any comments you want to share, please feel free to comment.


메타데이터
post_id
2f868e84cb3f
slug
go-pro-with-rxjs-2f868e84cb3f
url
https://medium.com/@mohammedfahimullah/go-pro-with-rxjs-2f868e84cb3f
canonical_url
https://medium.com/@mohammedfahimullah/go-pro-with-rxjs-2f868e84cb3f
author_url
https://medium.com/@mohammedfahimullah
status
ok
fetched_at
2026-09-04 21:40:00