I've to fetch data from an API which provides paged list of products. I want to fetch recursivly each page and then map each entry from response into another API call for getting each product details, yet getting the all products as a single stream of products. I've following -example- snippet.
import { flatMap } from 'rxjs/operators';
import { concat, forkJoin, from, of } from 'rxjs';
const productListPage1 = [1, 2, 3];
const productListPage2 = [5, 6, 7];
function products() {
return of({ data: productListPage1 }).pipe(
flatMap((res) => {
return concat(
// fetch details of each product
forkJoin(res.data.map((i) => of(i + 1))),
from(productListPage2)
);
})
);
}
products().subscribe((res) => console.log(res));
In this snippet productListPage1 is the first page, productListPage2 is next page. The result is following
[2, 3, 4]
5
6
7
while I need to achieve following results:
2
3
4
5
6
7
How could I modify above snippet to flatten the array [2, 3, 4] like rest of results?
Related
I have a problem with testing BehaviorSubject using rxjs marble.
Minimal reproduction:
scheduler.run(({ expectObservable }) => {
const response$ = new BehaviorSubject(1);
expectObservable(response$).toBe('ab', { a: 1, b: 2 });
response$.next(2);
});
Error:
Expected $.length = 1 to equal 2.
Expected $[0].notification.value = 2 to equal 1.
In my case response$ is returned from some service's method.
How can I test it?
Can you try switching the order and see if that helps?
scheduler.run(({ expectObservable }) => {
const data$ = new ReplaySubject<number>();
const response$ = new BehaviorSubject(1);
response$.subscribe(num => data$.next(num));
response$.next(2);
expectObservable(data$).toBe('(ab)', { a: 1, b: 2 });
});
I think with the expectObservable, that's when the observable is subscribed and tested.
Edit
You need to use ReplaySubject instead of BehaviorSubject because BehaviorSujbect only returns the last emission of the Subject. Check out my edit above. I got inspired by this answer here: https://stackoverflow.com/a/62773431/7365461.
I have recurring data structure (observable) returned from backend it looks like:
[{
id:1,
userId: 111,
name: '',
children :[
{
id:3,
userId: 333,
name: '',
children: [...]
}
]
},
{
id:2,
userId:111,
name:'',
children: [...]
}]
I have another end point that returns user name by user id. I need to call this service wit each of IDs and map returned name to the structure. Is there any pretty solution to achieve this using RxJs operators ?
You can try an approach like the following. See comments inline for details.
// fetchStruct is a function that returns an Observable which notifies the initial structure
const struct$ = fetchStruct();
// here we start the RxJs pipe transformation
struct$.pipe(
// when struct$ emits the initial structure we pass the control to another observable chain
// this is done via the concatMap operator
concatMap(beStruct => { // beStruct is the structure returned by the back end
// from beStruct we construct an array of Observables
// fetchName is a function that returns an Observable that emits when the name is returned
arrObs = beStruct.map(el => fetchName(el.id))
// with forkJoin we execute all the Observables in the array in parallel
forkJoin(arrObs).pipe(
// forkJoin emits when all Observables have notified and it will emit
// an array of values with the same order as arrObs
// we can therefore loop through this array to enrich beStruct with the names
map(names => {
names.forEach((n, i) => beStruct[i].name = n);
return beStruct;
})
)
})
)
This is quite a typical case with RxJs. You may find some other frequent pattern in this blog.
First I'm going to assume that .children can be arrays of ids i.e. pointers to other users. (See example below.)
What I'd would do is transform the initial users array into an array of observables. Each doing a request to map an id to a name. Use merge(…) to fire off the requests.
Of course you don't want a million of HTTP requests going on at the same time so you can set the concurrency parameter of merge to a number that is appropriate to you. (In my example I set it to 2.)
Since you had an array in, you want an array out. You can simply accumulate into an array with the toArray operator.
In the example below:
How I would process the initial users array (i.e. .children should be pointers)
Map users into an array of observables to resolve the names
Merge that array of observables with a concurrency set to 2
Accumulate the result into an array
const get_name = user => fake_backend[user.id].pipe(map(name => ({...user, name})));
// ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
// A B
//
// A: Simulate HTTP request latency to lookup an id and return a name
// B: Merge name into initial user object
merge(...users.map(get_name), 2).pipe(toArray()).subscribe(final_users => {
// ^ ^^^^^^^^^
// A B
//
// A: Concurrency. Maximum two HTTP requests at any one time.
// B: Accumulate each output into an array. Complete when merge is done.
console.log(final_users);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.2.0/rxjs.umd.min.js" integrity="sha512-MlqMFvHwgWJ1vfts5fdC2WzxDaIXWfYuAd9Tb2lobtF61Gk+HIRDrbtxgasBSM9lZgOK9ilwK9LqFIYEV+k0IA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const {merge, of} = rxjs;
const {tap, delay, map, toArray} = rxjs.operators;
const users =
[ { id: 1
, userId: 111
, name: ''
, children: [3]}
, { id: 2
, userId: 111
, name: ''
, children: [4]}
, { id: 3
, userId: 333
, name: ''
, children: []}
, { id: 4
, userId: 444
, name: ''
, children: []}];
const fake_backend =
{ 1: of('Foo').pipe(delay(3000), tap(() => console.log('user 1 ✓')))
, 2: of('Bar').pipe(delay(2000), tap(() => console.log('user 2 ✓')))
, 3: of('Baz').pipe(delay(1000), tap(() => console.log('user 3 ✓')))
, 4: of('Bat').pipe(delay(1000), tap(() => console.log('user 4 ✓'))) };
</script>
For your question here is a simple answer.
Steps:
From the list of Users end point you only need id's so you have to simplify the result from the users end point, instead of all the users data you only extract the id's from that end point and keep inside an array.
2.Convert the resultant array into another observable.
3.Send that into mergeMap and there you call a function with arguments to call your second URL service.
Here is a code snippet.
-//My first users service
let obsPattern1$ = this.http.get('https://reqres.in/api/users/');
obsPattern1$.pipe(
map(data=>data["data"].map(data=>{
return data.id
})),mergeMap(data=> data),mergeMap(data=>this.getUserFromHttp(data))
).subscribe(x=>console.log("final result",x))
}
//this is my second service function to fetch the individual item
getUserFromHttp(id) {
return this.http.get('https://reqres.in/api/users/' + id);
}
I have an array of objects in which one of the keys includes a customer id.
const customerArray = [{ customerId: 123, ...}, { customerId: 456, ...}];
I want to iterate through this array and make an api call to get further details about this customer from a separate endpoint.
const mapped = customerArray
.map(customer => ({
customerId: customer.customerId,
rating: this.productService(customer.customerId)
.pipe(map(rating => rating))}));
My expectation is that I would then have an array that includes an object with the following shape:
{
customerId: number,
rating: number
}
Instead, I end up with:
{
customerId: number,
rating: Observable
}
My productService call returns on observable and is used elsewhere in the app successfully. I need to have my map wait for the call to complete on the rating key before mapping to the next item in the array.
If I understand it right, you have to iterate through an array, make an http request to an endpoint for each element of the array, and fill each element of the array with the data returned by the endpoint.
So, if this is the case, you may try mergeMap like this
const myObs = from(customerArray).pipe(
mergeMap(customer => {
return this.productService(customer.customerId).pipe(
map(rating => ({customerId: customer.customerId, rating}))
)
})
)
If you subscribe to myObs you should get a stream of objects in the shape you are looking for, i.e.
{
customerId: number,
rating: number
}
mergeMap, previously known as flatMap, allows you to flatten a stream of Observables. In other words, if you iterate through an array to generate an array of Observables, which should be your case, mergeMap allows you to extract the values inside the Observables generated.
I got a stackblitz setup that shows a way you can manage it, but for the sake of keeping it always available
import { Observable, of, from } from 'rxjs';
import { map, mergeMap, combineAll } from 'rxjs/operators';
const custArray = [{customerId: 1}, {customerId: 2}, {customerId: 3}];
function mapSomeStuff(id: number): Observable<number> {
return of(id * id);
}
function doProductStuff(custArr: Array<{customerId: number}>): Observable<Array<{ customerId: number, rating: number}>> {
return from(custArray)
.pipe(
map(async (cust) => ({
customerId: cust.customerId,
rating: await mapSomeStuff(cust.customerId).toPromise()
})),
combineAll()
);
}
doProductStuff(custArray).subscribe(x => console.log(x))
This breaks up the array and creates an observable for each value in the array, runs the service, converts the observable to a promise and gets the final value of it, then combines all of the observables into a single observable with an array of values being the final out. You can check the output and see [Object, Object, Object] and check to see that the customerId and rating are available on each Object.
I'm composing multiple observables using an observable's pipe method and I'd like to emit a final composite value when all observables in the array emit.
import { apiSvc } from '../lib/api-service'
import { of as observableOf } from 'rxjs/observable/of'
import { map } from 'rxjs/operators'
const uris = [
'/api/items/1',
'/api/items/2',
'/api/items/3'
]
observableOf(uris).pipe(
// Map uris array to an array of observables.
map((uris) => calls.map(uri) => apiSvc.get(uri) /* returns observable*/),
// Perform magic and emit when all calls complete.
magic()
)
.subscribe((results) => {
console.log(results) // [{id: 1}, {id: 2}, {id: 3}]
})
I was able to make this work with forkJoin:
import { forkJoin } from 'rxjs/observable/forkJoin'
observableOf(uris).pipe(
// Map uris array to an array of observables.
map((uris) => calls.map(uri) => apiSvc.get(uri)),
)
.subscribe((requests) => {
// Emits when all request observables emit.
forkJoin(requests).subscribe((results) => {
console.log(results) // [{id: 1}, {id: 2}, {id: 3}]
})
})
...but I'm looking for a way to get it done in the pipe chain without having to nest subscribe calls.
The zip operator is sort of in the ballpark but it doesn't appear to work on arrays of observables. Is there a lettable operator that works like forkJoin and can be used with pipe?
You were very close. You want to return the forkJoined Observable inside the chain and wait until it emits with concatMap (mergeMap will work as well here).
observableOf(uris)
.pipe(
// Map uris array to an array of observables.
concatMap(uris => forkJoin(uris.map(uri => apiSvc.get(uri))),
)
.subscribe((responses) => {
...
});
I'm working with RxJs and I have to make a polling mechanism to retrieve updates from a server.
I need to make a request every second, parse the updates, emit it and remember its id, because I need it to request the next pack of updates like getUpdate(lastId + 1).
The first part is easy so I just use interval with mergeMap
let lastId = 0
const updates = Rx.Observable.interval(1000)
.map(() => lastId)
.mergeMap((offset) => getUpdates(offset + 1))
I'm collecting identifiers like this:
updates.pluck('update_id').scan(Math.max, 0).subscribe(val => lastId = val)
But this solution isn't pure reactive and I'm looking for the way to omit the usage of "global" variable.
How can I improve the code while still being able to return observable containing just updates to the caller?
UPD.
The server response for getUpdates(id) looks like this:
[
{ update_id: 1, payload: { ... } },
{ update_id: 3, payload: { ... } },
{ update_id: 2, payload: { ... } }
]
It may contain 0 to Infinity updates in any order
Something like this? Note that this is an infinite stream since there is no condition to abort; you didn't give one.
// Just returns the ID as the update_id.
const fakeResponse = id => {
return [{ update_id: id }];
};
// Fakes the actual HTTP call with a network delay.
const getUpdates = id => Rx.Observable.of(null).delay(250).map(() => fakeResponse(id));
// Start with update_id = 0, then recursively call with the last
// returned ID incremented by 1.
// The actual emissions on this stream will be the full server responses.
const updates$ = getUpdates(0)
.expand(response => Rx.Observable.of(null)
.delay(1000)
.switchMap(() => {
const highestId = Math.max(...response.map(update => update.update_id));
return getUpdates(highestId + 1);
})
)
updates$.take(5).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
To define the termination of the stream, you probably want to hook into the switchMap at the end; use whatever property of response to conditionally return Observable.empty() instead of calling getUpdates again.