Table of contents
Cache issues
Among all bugs, cache issues take first place. If somehow, for some reason, you hadn’t fixed bugs related to cache, then you’re lucky.
It often happens when you’re using RxJS subjects, especially
During this article, we’ll try to address this kind of issue. Furthermore, we’re going to develop a new subject for such needs. So if you’re interested, stay tuned!
Suppose that we’re using RxJS with Angular. Firstly, you read params from the route and store them in the subject.
Secondly, the user leaves the URL, and Angular destroys the component.
Regardless of component destructuring, service wasn’t destroyed yet, as was provided in the parent component.
Thus the previously stored value stays in it. Unless you cleaned it up using the initial one.
Finally, once the user opens the same component with the different route params, the first emitted value from the subject will be a cached value.
Moreover, if the
Here is how the issue looks in the real world:
As you might notice, starting from the second request, there are two of them. User id in the first of two requests comes from the cache.
Source
In the component, we read
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { UserService } from '../user.service';
import { UserModel } from '../user.model';
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.scss'],
})
export class UserDetailsComponent implements OnInit, OnDestroy {
private readonly destroySubj = new Subject<void>();
user!: UserModel;
constructor(
private route: ActivatedRoute,
readonly userService: UserService
) {}
ngOnInit(): void {
this.userService.loadUser$
.pipe(takeUntil(this.destroySubj))
.subscribe((user: UserModel) => {
this.user = user;
});
this.route.paramMap.subscribe((params) => {
this.userService.setUserId(Number(params.get('userId')));
});
}
ngOnDestroy(): void {
this.destroySubj.next();
this.destroySubj.complete();
}
}
<div class="main-container">
<a routerLink="/">Home</a>
<br>
<ng-container *ngIf="(userService.userId$ | async) == user?.id; else loading">
<div class="card">
<div>id: {{ user.id }}</div>
<div>first_name: {{ user.first_name }}</div>
<div>email: {{ user.email }}</div>
</div>
</ng-container>
<ng-template #loading>
Loading ...
</ng-template>
</div>
Service is responsible for storing
import { Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { UserWebService } from './user-web.service';
@Injectable({
providedIn: 'root',
})
export class UserService {
userIdSubj = new ReplaySubject<number>(1);
userId$ = this.userIdSubj.asObservable();
loadUser$ = this.userId$.pipe(
mergeMap((userId: number) => {
return this.userWebService.getUser(userId);
})
);
constructor(private userWebService: UserWebService) {}
setUserId(userId: number): void {
this.userIdSubj.next(userId);
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { delay, map } from 'rxjs/operators';
import { UserModel } from './user.model';
@Injectable({
providedIn: 'root',
})
export class UserWebService {
constructor(private httpClient: HttpClient) {}
getUser(userId: number): Observable<UserModel> {
const possibleDelay = userId === 2 ? '?delay=3' : '';
return this.httpClient
.get(`https://reqres.in/api/users/${userId}${possibleDelay}`)
.pipe(map((user: any) => user.data));
}
}
export interface UserModel {
id: number;
first_name: string;
email: string;
}
Now, let’s try to tackle the issue.
What is the solution:
First, let’s take a look at it in action with a
Works as expected.
In service, we made the following updates:
- We changed
mergeMap toswitchMap - Instead of
ReplaySubject we used custom subject —ResettableSubject - We added a
resetUserId method that will cleanuserId in the service
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs/operators';
import { ResettableSubject } from '../shared/resettable-subject';
import { UserWebService } from './user-web.service';
@Injectable({
providedIn: 'root',
})
export class UserService {
userIdSubj = new ResettableSubject<number>();
userId$ = this.userIdSubj.asObservable();
loadUser$ = this.userId$.pipe(
switchMap((userId: number) => {
return this.userWebService.getUser(userId);
})
);
constructor(private userWebService: UserWebService) {}
setUserId(userId: number): void {
this.userIdSubj.next(userId);
}
resetUserId(): void {
this.userIdSubj.reset();
}
}
In the component, we only added one line where we reset
ResettableSubject
And the final piece of code is our
import { Observable, ReplaySubject, Subject, Subscription } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';
export class ResettableSubject<T> extends Subject<T> {
private modifierSubj = new Subject<T>();
private subscription: Subscription;
private factoryResultSubj: Subject<T>;
private factoryFn: () => Subject<T>;
private value$: Observable<T>;
constructor(factoryFn: () => Subject<T> = () => new ReplaySubject<T>(1)) {
super();
this.factoryFn = factoryFn;
this.factoryResultSubj = this.factoryFn();
this.subscription = this.modifierSubj.subscribe(this.factoryResultSubj);
this.value$ = this.pipe(
startWith(undefined),
switchMap(() => this.factoryResultSubj)
);
}
asObservable(): Observable<T> {
return this.value$;
}
reset(): void {
this.subscription.unsubscribe();
this.next((undefined as any) as T);
this.factoryResultSubj = this.factoryFn();
this.subscription = this.modifierSubj.subscribe(this.factoryResultSubj);
}
next(value: T): void {
this.modifierSubj.next(value);
}
}
It adds the ability to roll back subject value to the initial state. Therefore, we don’t need to track cached values anymore.
Other possibilities
There are also other options to implement a cache cleaning pattern. For instance, instead of using
Here’s a brief overview:
import { Observable, Subject } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';
export type ResettableType<T> = [Observable<T>, Subject<T>, () => void];
export function asResettable<T>(factory: () => Subject<T>): ResettableType<T> {
const resetSubj = new Subject<T>();
const modifySubj = new Subject<T>();
let newValueSubj = factory();
let subscription = modifySubj.subscribe(newValueSubj);
return [
resetSubj.asObservable().pipe(
startWith(undefined),
switchMap(() => newValueSubj)
),
modifySubj,
() => {
subscription.unsubscribe();
newValueSubj = factory();
subscription = modifySubj.subscribe(newValueSubj);
resetSubj.next((undefined as any) as T);
},
];
}