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