Reputation: 459
I have an application that gets random jokes every 10 seconds. I'm using interval operator from rxjs for countdown and after 10 seconds i make a http request to get a random joke. The issue is i'm displaying jokes twice in my template using async pipe. I know that doing that will create two new subscriptions and make http request twice. To handle that i tried using shareReplay or publish + refCount , so that only one http request is made. But it calls getRandomJoke service function twice. How to fix this issue.
Code and Demo - https://stackblitz.com/edit/angular-ivy-xvtsrw
random-joke.component.html
<div class="joke-title">{{joke | async}}</div>
<div class="joke-title">{{joke | async}}</div>
<div class="footer">Next Joke in : {{countdown | async}}</div>
random-joke.component.ts
export class RandomJokesComponent implements OnInit {
joke: Observable<any>;
restartTimer = new Subject();
restartInterval = new Subject();
intervalForJokes: Observable<any>;
countdown: any;
countDownTill = 10;
constructor(private fetchService: FetchUtilService) {}
ngOnInit() {
this.startTimer();
this.getJokesInInterval();
}
getJokesInInterval() {
this.restartInterval.next();
let intervalForJokes = interval(10000);
this.joke = intervalForJokes.pipe(
tap(()=> console.log('getting interval')),
takeUntil(this.restartInterval),
publish(),
refCount(),
switchMap(() =>
this.fetchService.getRandomJoke().pipe(
tap(() => this.startTimer()))
)
);
}
startTimer() {
this.restartTimer.next();
this.countdown = timer(0, 1000).pipe(
takeUntil(this.restartTimer),
map(i => this.countDownTill - i)
);
}
}
fetch-service
getRandomJoke(): Observable<any> {
console.log('getting');
return this.http.get(this.apiUrl).pipe(
tap(result => console.log(result)),
// shareReplay(),
publish(),
refCount(),
map((result: any) => result && result.value.joke)
);
}
Upvotes: 0
Views: 236
Reputation: 461
Your implementation is overcomplicated.
Use shareReplay
and start joke fetching immediately by using startWith(0)
.
FetchUtilService.getRandomJoke
can be simplified just to http request logic + error handling.
ngOnInit() {
this.startTimer();
this.joke = interval(10000).pipe(
startWith(0),
switchMap(() =>
this.fetchService.getRandomJoke().pipe(tap(() => this.startTimer()))
),
shareReplay(),
);
}
@Injectable()
export class FetchUtilService {
private apiUrl = 'https://api.icndb.com/jokes/random';
constructor(private http: HttpClient) {}
getRandomJoke(): Observable<any> {
console.log('getRandomJoke invocation');
return this.http
.get(this.apiUrl)
.pipe(map((result: any) => result && result.value.joke));
}
}
Demo: https://stackblitz.com/edit/angular-ivy-qpugke
Bonus: restart timer on mouse click
ngOnInit(): void {
const click$ = fromEvent(document, 'click').pipe(
debounceTime(400),
startWith(null), // init timer
);
this.timer$ = click$.pipe(
switchMap(() => interval(1000)), // kills interval on click and starts new one
);
const readyToFetch$ = this.timer$.pipe(
map((seconds) => seconds > 0 && seconds % 10 === 0), // fetch every 10 seconds, skip 0 to avoid joke's fetching on timer restart
);
this.joke$ = readyToFetch$.pipe(
startWith(true), // initial fetch
filter((readyToFetch) => readyToFetch),
switchMap(() => this.fetchService.getRandomJoke()),
shareReplay(),
);
}
Demo: https://stackblitz.com/edit/angular-ivy-uak1sx
Upvotes: 3
Reputation: 17762
If you want to use the same response of an http call, you better think to use Subject
s.
The mechanism can be the following.
First you create a service which exposes a method, for intance callRemoteService
, to fire the http call and an Observable, for instance httpResp
, that will emit the response. httpResp$
is obtained tranforming a private Subject, for instance _resp$
into an Observable using the asObservable()
method of Subject.
Then, when the Obvervable linked to the http call emits, you make the internal Subject _resp$
emit as well.
More than one client can subscribe to the public httpResp$
Observable and will receive the same notification when the http call returns.
The code could look like this
private _resp$ = new Subject<any>();
public httpResp$ = this._resp$.asObservable();
public callRemoteService() {
return this.http.get(this.apiUrl).pipe(
tap({
next: (data) => this._resp$.next(data),
error: (err) => this._resp$.error(err)
})
).subscribe()
}
You may find more inspiration in this article. It talks about React, but the mechanism illustrated there can be applied to Angular too.
Upvotes: 0