Search code examples
angularrxjsangular-materialrxjs-observables

TypeError: Cannot read properties of undefined (reading 'filter') in Angular RxJS subscription


I'm encountering a TypeError in my Angular component with RxJS observables. The error message "TypeError: Cannot read properties of undefined (reading 'filter')" is pointing to my usage of filter on an array.

The error specifically happens within the userService.users$ subscription when attempting to filter this.users. The intention is to exclude the logged-in user from the list.

What should I do differently?

Here is a simplified version of the relevant code:

user-list.component.ts

export class UserListComponent implements OnInit, OnDestroy {
  protected readonly Object = Object;
  users!: User[];
  usersSubscription!: Subscription;
  loggedUser!: User;
  loggedUserSubscription!: Subscription;
  groupedAndSortedUsers!: { [key: string]: User[] };

  constructor(public userService:UserService) {
  }

  ngOnInit() {

    this.loggedUserSubscription = this.userService.loggedUser$.subscribe(user => {
      this.loggedUser = user;
    })

    this.userService.users$.subscribe(users => {
        this.users = users.filter(user => user?.id !== this.loggedUser.id);
        // Remove the current logged-in user
        const filteredUsers = this.users.filter(user => user?.id !== this.loggedUser.id);
        this.groupedAndSortedUsers = this.userService.groupAndSortUsers(filteredUsers);
    });
  }

  ngOnDestroy() {
    this.usersSubscription.unsubscribe();
    this.loggedUserSubscription.unsubscribe();
  }
}

user.service.ts

export class UserService {
  private _users$: BehaviorSubject<User[] | undefined> = new BehaviorSubject<User[] | undefined>(undefined);
  private _loggedUser$: BehaviorSubject<User | undefined> = new BehaviorSubject<User | undefined>(undefined);

  constructor(private userHttpService: UserHttpService, private router: Router, private dtoMapperService: DtoMapperService, private matSnackBar: MatSnackBar,) {
    this.fetchUsers();
    this.fetchLoggedUser();
  }

  public get users$(): Observable<User[]> {
    return this._users$.asObservable() as Observable<User[]>;
  }

  public get users(): User[] {
    return this._users$.getValue() as User[];
  }

  public get loggedUser$(): Observable<User> {
    return this._loggedUser$.asObservable() as Observable<User>;
  }

  public get loggedUser(): User {
    return this._loggedUser$.getValue() as User;
  }

  public set users(users: User[]) {
    this._users$.next(users);
  }

  public set loggedUser(user: User) {
    this._loggedUser$.next(user);
  }

  public fetchUsers() {
    this.userHttpService.fetchUsers().subscribe({
      next: (usersDtos: UserDto[]) => {
        const users = usersDtos.map(userDto => this.dtoMapperService.mapUserDtoToUser(userDto));
        this._users$.next(users);
      }
    });
  }

  public fetchLoggedUser() {
    this.userHttpService.fetchLoggedUser().subscribe({
      next: (userDto: UserDto) => {
        this._loggedUser$.next(this.dtoMapperService.mapUserDtoToUser(userDto))
      }
    })
  }

  public groupAndSortUsers(users: User[]): { [key: string]: User[] } {
    const sortedUsers: { [key: string]: User[] } = {};

    users.forEach(user => {
      const firstLetter = user.name.charAt(0).toUpperCase();
      if (!sortedUsers[firstLetter]) {
        sortedUsers[firstLetter] = [];
      }
      sortedUsers[firstLetter].push(user);
    });

    const sortedKeys = Object.keys(sortedUsers).sort();

    sortedKeys.forEach(letter => {
      sortedUsers[letter].sort((a, b) => a.name.localeCompare(b.name));
    });

    const sortedUsersResult: { [key: string]: User[] } = {};
    sortedKeys.forEach(letter => {
      sortedUsersResult[letter] = sortedUsers[letter];
    });

    return sortedUsersResult;
  }
}

user-list.component.ts

<div class="user-list-box">
    <mat-list-item role="listitem">ME</mat-list-item>
    <mat-divider></mat-divider>
    <app-user [user]="userService.loggedUser"></app-user>
    
    <mat-list role="list">
      @for (letter of Object.keys(groupedAndSortedUsers); track letter) {
        <mat-list-item role="listitem">{{ letter }}</mat-list-item>
        <mat-divider></mat-divider>
        @for (user of groupedAndSortedUsers[letter]; track user.id) {
          <app-user [user]="user"></app-user>
        }
     }
    </mat-list>
</div>

Solution

  • You can use the switchMap operator from RxJS to ensure that you have the loggedUser before you subscribe to users$. The switchMap operator maps each value to an Observable, then it flattens all of these inner Observables using switch.

    Here’s how you can modify your ngOnInit method:

    ngOnInit() {
      this.loggedUserSubscription = this.userService.loggedUser$.pipe(
        switchMap((loggedUser) => { // <-- Add this line
          this.loggedUser = loggedUser;
          return this.userService.users$;
        })
      ).subscribe(users => { <-- only when loggedUser is not null or undefined
        this.users = users.filter(user => user?.id !== this.loggedUser.id);
        const filteredUsers = this.users.filter(user => user?.id !== this.loggedUser.id);
        this.groupedAndSortedUsers = this.userService.groupAndSortUsers(filteredUsers);
      });
    }