I have an rxjs observable which is already subscribed to:
const o = new rxjs.Subject();
o.subscribe( (v) => console.log("value: " + v);
Due to some unrelated event, e.g. a user clicking on a button, I need to go and fetch data from the server, and put that data into the existing observable o. Using axios, I have something like this:
const aPromise = axios.get("someurl...")
I then do the following:
const secondObservable = rxjs.of(aPromise)
.subscribe( (response) => o.next(response.data) )
My question is whether there is a better way to put the response data into the existing already subscribed to observable, perhaps using operators?
If you know that you need data from your original stream and might get updates from a button click — then we'll need to create a stream of button clicks, e.g.
const btnClick$ = new rxjs.Subject();
onClick(){
this.btnClick$.next(void 0);
}
Then we'll turn this btnClick$ stream into a http-get request stream, using switchMap:
const request$ = btnClick$.pipe(
// each click will be turned into a http-get
switchMap(() => rxjs.of(axios.get("someurl...")))
)
And we're ready to consume results from both streams, original and a stream of possible data updates from server on button click:
// we'll take results both from original stream
// and our new possible source of data
merge(original$, request$).subscribe(data => {
console.log(data);
})
Heres a little app using mocks:
const { fromEvent, of, merge } = rxjs;
const { map, delay, switchMap } = rxjs.operators;
// create a stream of btn clicks
const theButton = document.getElementById('the-btn');
const btnClicks$ = fromEvent(theButton, 'click');
console.log('Processing...');
// clicks turned into http-gets
const request$ = btnClicks$.pipe(
switchMap(() => makeRequest())
);
// original source of data
const original$ = of('Please, click the button!').pipe(delay(500));
merge(original$, request$)
.subscribe(data => {
console.log(data);
});
// mock for requests
function makeRequest(){
return of(Date.now()).pipe(delay(500))
}
<script src="https://unpkg.com/rxjs#6.4.0/bundles/rxjs.umd.min.js"></script>
<button
id="the-btn"
>Click me!</button>
Hope this helps
You can just pass the result of the call directly to your observable.
const axios = {
get: url => Promise.resolve('some value')
};
const o = new rxjs.Subject();
o.subscribe(v => { console.log("value: " + v); });
axios.get('url').then(data => { o.next(data); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
Related
I have a Subject that maps an observable, I subscribe to the observable in the concatMap but it doesn't trigger tap() from the subscription.
this.streamA$ = this.streamService.getStream(1)
.pipe(
tap(data => console.log('stream data:', data))
);
Subject
this.images$ = this.queue.pipe(concatMap((event: Observable<string>) => {
// when an event arrives here it is still wrapped in observable
// subscribe isn't triggering tap()
event.subscribe(data => {
//console.log('inner observable subscription:', data);
});
// observable goes to the image$ observable, it is unwrapped by: image$ | async in template
return event;
}));
Service function
getStream(time: number): Observable<string> {
let timer$ = timer(2000);
console.log('get stream');
const observable = new Observable<string>(observer => {
timer$.pipe(
map(() => {
observer.next('http response 1');
observer.complete();
})
).subscribe();
});
return observable;
}
Update:
event.subscribe(data => {
//console.log('inner observable subscription:', data);
});
Without console.log included in subscribe this is the output:
stream data: http response 1
With console.log, prints these 3 lines at the same time:
stream data: http response 1
inner observable subscription: http response 1
stream data: http response 1
When you have an Observable inside a concatMap, you don't need to subscribe. Could you try something along these lines?
this.images$ = this.queue.pipe(
concatMap(event => event), // I assume the event here is something like this.streamA$
tap(data => // do stuff with data)
);
I have this code:
async download(fileToUpload: UploadedFileMetaData): Promise<Observable<DownloadEvent>> {
const url = await this.getDownloadUrl(fileToUpload);
let xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
return Observable.create((observer) => {
console.log(observer);
xhr.open('GET', url);
xhr.send();
xhr.addEventListener('progress', (progress) => {
let percentCompleted;
That returns an ovservable.
I then use it like this:
const downloadSubscription = await this.blobStorageService.download(file);
downloadSubscription.subscribe((event) => // do stuff
Multiples of these might be created.
How do I unsubscribe?
You can store the subscribe() method return value which is a Subscriber object in a variable and call the unsubscribe() method when you want to unsubscribe.
const downloadSub = downloadSubscription.subscribe((event) => {});
downloadSub.unsubscribe();
In the end I went with this:
const destroy$ = new Subject<boolean>();
downloadSubscription.takeUntil(destroy$).subscribe(
// main body
},
(downloadEvent: FailureDownloadEvent) => {
// error
destroy$.next(true);
destroy$.unsubscribe();
},
() => {
// cleanup
destroy$.next(true);
destroy$.unsubscribe();
}
);
}
I'm utilising the following API for a World Cup Laravel app - http://api.football-data.org/docs/v1/index.html#_fixture
This information brings me back today's fixture's as I'm using this code (config just holds my API key):
const todaysMatches = new Vue({
el: '#todaysMatches',
data: {
todaysMatches: [],
flags: []
},
methods: {
loadData: function () {
axios.get("http://api.football-data.org/v1/competitions/467/fixtures/?timeFrame=p1", config)
.then(response => {this.todaysMatches = response.data});
}
},
mounted: function () {
this.loadData();
}
});
This brings back the following data sctructure:
Inside each fixture you get an array of _links which you can see in the below screenshot:
Now, what I would like to do is query both the awayTeam api and the homeTeam api because they each have an endpoint of crestUrl which returns the country's flag.
You can see that inside my data I've set an array prop called flags so I was thinking of running additional calls inside my loadData method and populate that array for each fixture, but I don't think that's a clean way of doing it.
Can anyone suggest the best way to approach this?
I have used async/await pattern to achieve your requirement as below:
loadData: async function() {
const response = await axios.get(
"http://api.football-data.org/v1/competitions/467/fixtures/?timeFrame=p1",
config
);
this.todaysMatches = response.data;
let arr = this.todaysMatches.fixtures.map(fixture => {
const _links = fixture._links;
return [
axios.get(_links.awayTeam.href, config),
axios.get(_links.homeTeam.href, config)
];
});
arr.forEach(async item => {
const away = await item[0];
const home = await item[1];
this.flags.push({
awayFlag: away.data.crestUrl,
homeFlag: home.data.crestUrl
});
});
}
Explaination:
After fetching todaysMatches a new array arr is created which consists of promises returned by get request to the team's url [[getAwayTeamInfo, getHomeTeamInfo], [getAwayTeamInfo, getHomeTeamInfo], [getAwayTeamInfo, getHomeTeamInfo],...]
We loop through this and await on the promise to get the crestUrl
This crestUrl is pushed into flags array as an object
{
awayFlag: away.data.crestUrl,
homeFlag: home.data.crestUrl
}
Update
Adding the flag urls directly to the this.todaysMatches.fixtures array
loadData: async function() {
const response = await axios.get(
"http://api.football-data.org/v1/competitions/467/fixtures/?timeFrame=p1",
config
);
this.todaysMatches = response.data;
const fixtures = this.todaysMatches.fixtures;
let arr = fixtures.map(fixture => {
const _links = fixture._links;
return [
axios.get(_links.awayTeam.href, config),
axios.get(_links.homeTeam.href, config)
];
});
arr.forEach(async (item, index) => {
const away = await item[0];
const home = await item[1];
this.$set(fixtures, index, {
...fixtures[index],
awayFlag: away.data.crestUrl,
homeFlag: home.data.crestUrl
});
});
}
I am making an API call through an Observable. If this API call takes more than 200ms, I would like to show a loading screen (by assigning 'true' to my 'loading' variable), otherwise I don't want to show anything, in order to avoid a blink on screen.
Is there an RxJS operator capable of doing this ?
this.apiService.get(`/api/someEndpoint`)
// I hope for something like
.triggerIfAtLeastThisAmountOfTimeHasElapsed(200, () => {
this.loading = true;
})
.subscribe(response => {
// Process the response
this.loading = false;
});
There are many ways to do this so you can use for example this:
const api = this.apiService.get(`/api/someEndpoint`);
const loading = Observable
.timer(1000)
.do(() => loading = true) // show loading
.ignoreElements(); // or `filter(() => false)
Observable.merge(api, loading)
.take(1)
.subscribe(() => loading = false);
Along the same lines of Martin's response, this is an example that should simulate your context
const obs1 = Observable.timer(200).take(1);
const apiSubject = new Subject<string>();
const apiObs = apiSubject.asObservable();
const apiExecutionElapsed = 1000;
const obs3 = Observable.merge(obs1, apiObs);
let loading = undefined;
obs3.subscribe(
data => {
console.log(data);
if (loading === undefined && data === 0) {
loading = true;
} else {
loading = false;
}
console.log('loading', loading);
},
console.error,
() => {
loading = false;
console.log('loading', loading);
}
)
setTimeout(() => {
apiSubject.next('I am the result of the API');
apiSubject.complete()}, apiExecutionElapsed)
If the execution of the api (apiExecutionElapsed) takes longer than the configured timer (200 ms in this case) you see the loading flag to become first true and then false. Otherwise it remains always false.
I am new to Rxjs and am trying to implement the following workflow in it:
User clicks on a menu item that triggers an HTTP request
Before the response has arrived, the user clicks on a second request
The subscription to the first request is ended and a subscription to the second request is started
// The code below sits inside the onClick event of my menu
var callAction = function(someParameters) {
return Rx.Observable.create(function(observer) {
var subscribed = true;
myHttpApi.someActionCall(someParameters).then(
(data: any) => {
if (subscribed) {
// Send data to the client
observer.next(data);
// Immediately complete the sequence
observer.complete();
}
}).catch((err: any) => {
if (subscribed) {
// Inform the client that an error occurred.
observer.error(ex);
}
}
);
return function () {
subscribed = false;
}
});
};
The observer is further defined below:
var observer = {
// onNext in RxJS 4
next: function (data) {
// Do what you need to do in the interface
},
// onError in RxJS 4
error: function (err) {
// Handle the error in the interface
},
// onComplete in RxJS 4
complete: function () {
//console.log("The asynchronous operation has completed.");
}
};
let subscription = callAction(somParameters).subscribe(observer);
How do I now go about implementing #3, whereby the subscription to the first request is ended and a subscription to the new request (in this example, the same block of code is executed for different menu options and therefore different requests based on the parameters) is started?
Breaking up the steps into discrete functions,
// Inner observable, calls the API
const callAction$ = function(someParameters) {
return Observable.fromPromise(
myHttpApi.someActionCall(someParameters)
)
}
// Outer observable, controls the click chain
const click$ = new Subject();
click$.switchMap(clickParams => {
return callAction$(clickParams)
})
.subscribe(
result => console.log('Result: ', result),
err => console.log('Error: ', err.message)
)
// Handler function, called from menu
const handleClick = function(clickParams) {
click$.next(clickParams)
}
Working example CodePen