I am using RxJS Ajax() to return a bunch of GitHub users. I want to iterate over each one and just get the login names from each user. I am able to iterate over a local array of objects, but not if that array of objects came from Ajax, whether or not I force it to be a stream with RxJS from().
// from() the array of objects (locally defined)
let localArrayOfObj = from([
{ login: 'Barbara Holland', loggedIn: false, token: null },
{ login: 'Joyce Byers', loggedIn: true, token: 'abc' },
{ login: 'Nancy Wheeler', loggedIn: true, token: '123' }
]);
// Return the "login" names:
localArrayOfObj.pipe(
map((data) => data.login)
).subscribe(console.log); // This returns the login names
// from() the array of objects (returned by Ajax)
const githubUsers = `https://api.github.com/users?per_page=2`;
const arrayFromAjax = from(ajax.getJSON(githubUsers));
arrayFromAjax.subscribe(console.log);
// Trying the same to return the "login" names:
arrayFromAjax.pipe(
map((data) => data.login)
).subscribe(console.log); // This is undefined
I'm putting the results of the Ajax call through RxJS from() so both arrays are treated the same, however the same undefined results occur if I skip from() and just do
const arrayFromAjax = ajax.getJSON(githubUsers);
instead of
const arrayFromAjax = from(ajax.getJSON(githubUsers));
How is an array of objects from Ajax() different than a similar locally defined array of objects? And how can I iterate over the Ajax results?
The from() operator creates an observable that emits the provided values one at at time, so you are not receiving an array, you are receiving individual objects.
ajax.getJSON(githubUsers) returns an array of objects. This is why you are seeing differences between these two cases.
You can alter your test case with local array to the following the match the same shape:
let localArrayOfObj = from([
[
{ login: 'Barbara Holland', loggedIn: false, token: null },
{ login: 'Joyce Byers', loggedIn: true, token: 'abc' },
{ login: 'Nancy Wheeler', loggedIn: true, token: '123' }
]
]);
In your code here: data is an array, so if you want to modify the elements individually you can do something like:
arrayFromAjax.pipe(
map((dataArray) => dataArray.map(data => data.login))
).subscribe(console.log); // This is undefined
Related
There is an array in public users = new BehaviorSubject<User[]>([]).
I want to delete element from this observable and refresh it.
My solution is:
const idRemove = 2;
this.users.next(this.user.getValue().filter((u) => u.id !== idRemove)
But I seem I use wrong way of using RXJS
Toward Idiomatic RxJS
Using subscribe instead of .value.
interface User {
id: number
}
const users$ = new BehaviorSubject<User[]>([
{id:1},
{id:2},
{id:3}
]);
function removeId(idRemove: number) {
users$.pipe(
take(1),
map(us => us.filter(u => u.id !== idRemove))
).subscribe(
users$.next.bind(users$)
);
}
users$.subscribe(us =>
console.log("Current Users: ", us)
);
removeId(2);
removeId(1);
removeId(3);
Output:
Current Users: [ { id: 1 }, { id: 2 }, { id: 3 } ]
Current Users: [ { id: 1 }, { id: 3 } ]
Current Users: [ { id: 3 } ]
Current Users: []
To handle state within RxJS pipes you can use the Scan operator
Useful for encapsulating and managing state. Applies an accumulator (or "reducer function") to each value from the source after an initial state is established -- either via a seed value (second argument), or from the first value from the source.
const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;
// This function is used to apply new users to the state of the scan
const usersFn = users => state => users
// This function is used to remove all matching users with the given id from the state of the scan
const removeFn = removeId => state => state.filter(user => user.id !== removeId)
// This Subject represents your old user BehaviorSubject
const users$$ = new Subject()
// This Subject represents the place where this.users.next(this.user.getValue().filter((u) => u.id !== idRemove) was called
const remove$$ = new Subject()
// This is your new user$ Observable that handles a state within its pipe. Use this Observable in all places where you need your user Array instead of the user BehaviorSubject
const user$ = merge(
// When users$$ emits the usersFn is called with the users argument (1. time)
users$$.pipe(map(usersFn)),
// When remove$$ emits the removeFn is called with the removeId argument (1. time)
remove$$.pipe(map(removeFn))
).pipe(
// Either the usersFn or removeFn is called the second time with the state argument (2. time)
scan((state, fn) => fn(state), [])
)
// Debug subscription
user$.subscribe(console.log)
// Test emits
users$$.next([
{id: 1, name: "first"},
{id: 2, name: "second"}
])
remove$$.next(2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
Ben Lesh (main Contributor of RxJS) wrote an anser about why not to use getValue in RxJS: The only way you should be getting values "out of" an Observable/Subject is with subscribe!
Using getValue() is discouraged in general for reasons explained here even though I'm sure there are exception where it's fine to use it. So a better way is subscribing to get the latest value and then changing it:
this.users
.pipe(take(1)) // take(1) will make sure we're not creating an infinite loop
.subscribe(users => {
this.users.next(users.filter((u) => u.id !== idRemove);
});
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 am running the following code:
let Payment = relevantWaitList.map(e => {
stripe.paymentIntents.create({
amount: Math.round(e.totalCharge * 100),
currency: currency,
description: `Resale of ${eventData.title} refunded ticket`,
// customer: customerStripeID,
payment_method: e.paymentMethod,
off_session: true,
confirm: true,
application_fee_amount: Math.round(e.applicationFee*100)
}
,{
stripe_account: organiserStripeAccountID,
}
)
})
Promise.all(Payment)
.then(data => {
console.log('promiseall payment res', data)
}).catch(err => {
console.log('promise all payment fail', err)}
Which is returning the following:
promiseall payment res undefined
Despite it returning undefined, the promise.all is working - the stripe payments intents are created.
When I change to promise to include the .then within the map (using the code below), it console logs fine but I would prefer to play with the data after all promises have been completed.
What am I missing?
let Payment = relevantWaitList.map(e => {
stripe.paymentIntents.create({
amount: Math.round(e.totalCharge * 100),
currency: currency,
description: `Resale of ${eventData.title} refunded ticket`,
// customer: customerStripeID,
payment_method: e.paymentMethod,
off_session: true,
confirm: true,
application_fee_amount: Math.round(e.applicationFee*100)
}
,{
stripe_account: organiserStripeAccountID,
}
)
.then(data => console.log('data within map', data))
.catch(err => console.log('err within map', err))
})
Your .map() callback does not return anything which means that .map() will just return an array of undefined, giving Promise.all() no promises and no data to use.
What you need to be passing Promise.all() is an array of promises that each resolve to a value, then Promise.all() will return a promise that will resolve to an array of those values. In this case, you have garbage into Promise.all() and therefore garbage out.
So, your .map() callback should be returning a promise that resolves to the value you eventually want.
Assuming stripe.paymentIntents.create() returns a promise that resolves to a value you want, you just need to add a return statement:
let Payment = relevantWaitList.map(e => {
// ******* Add return on next line *********
return stripe.paymentIntents.create({
amount: Math.round(e.totalCharge * 100),
currency: currency,
description: `Resale of ${eventData.title} refunded ticket`,
// customer: customerStripeID,
payment_method: e.paymentMethod,
off_session: true,
confirm: true,
application_fee_amount: Math.round(e.applicationFee*100)
} , {stripe_account: organiserStripeAccountID,
});
});
As mentioned in the answer from #jfriend00, your map callback does not return anything.
You need to return the Promise object for each iteration
const payments = relevantWaitList.map(e => {
return stripe.paymentIntents.create({
// your props
});
});
The payments variable now contains an array of promises. You can use Promise.all() now
Promise.all(payments).then(data =>{
// play with data here :)
});
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 am new to cyclejs and rxjs in general and was hoping someone could help me solve my problem.
I am trying to build a demo application for my understanding and stuck with rendering JSON objects on the DOM.
My demo application calls the NASA near earth objects API for the past 7 days and tries to display them.
There is a Load More button at the bottom which on clicking will load data of the previous 7 days (Today - 7 upto Today - 14).
The response I get from the API is as follows
{
"links" : {
"next" : "https://api.nasa.gov/neo/rest/v1/feed?start_date=2016-09-06&end_date=2016-09-12&detailed=false&api_key=DEMO_KEY",
"prev" : "https://api.nasa.gov/neo/rest/v1/feed?start_date=2016-08-25&end_date=2016-08-31&detailed=false&api_key=DEMO_KEY",
"self" : "https://api.nasa.gov/neo/rest/v1/feed?start_date=2016-08-31&end_date=2016-09-06&detailed=false&api_key=DEMO_KEY"
},
"element_count" : 39,
"near_earth_objects" : {
"2016-09-06" : [{
some data
},
{
some data
}],
2016-08-31: [{...}],
...
}
}
I am interested in near_earth_objects JSON object but I am unable to map it beacause of it being an Object.
How do I handle such a situations? Below is the code that I have
function main(sources) {
const api_key = "DEMO_KEY";
const clickEvent$ = sources.DOM.select('.load-more').events('click');
const request$ = clickEvent$.map(() => {
return {
url: "https://api.nasa.gov/neo/rest/v1/feed?start_date=2015-09-06&end_date=2016-09-13&api_key=" + api_key,
method: "GET"
}
}).startWith({
url: "https://api.nasa.gov/neo/rest/v1/feed?start_date=2016-08-31&end_date=2016-09-06&api_key=" + api_key,
method: "GET"
});
const response$$ = sources.HTTP.filter(x$ => x$.url.indexOf("https://api.nasa.gov/neo/rest/v1/feed") != -1).select(response$$);
const response$ = response$$.switch(); //flatten the stream
const nasa$ = response$.map(response => {
return response.body
});
const sinks = {
DOM: nasa$.map(nasa =>
([nasa.near_earth_objects]).map(objects => {
var vdom = [];
//I am not very happy with this part. Can this be improved?
for (var key in objects) {
if (objects.hasOwnProperty(key)) {
vdom.push(objects[key].map(obj => div([
h1(obj.name)
])))
}
}
//returning the vdom does not render on the browser. vdom is an array of arrays. How should i correct this?
console.log(vdom);
return vdom;
})
),
HTTP: request$
};
return sinks;
};
Conceptually, you want to extract the entries of nasa.near_earth_objects (i.e., turn the Object into an Array), then flat map that Array into an Observable sequence.
I'll assume you're already using lodash in your project (you can do it without lodash, but you'll just need to write more glue code manually). I'll also assume you're importing RxJS' Observable as Rx.Observable; adjust the names below to suite your code.
You can accomplish the first task using _.toPairs(nasa.near_earth_objects), and the second part by calling .flatMap(), and returning Rx.Observable.from(near_objects). The resulting Observable will emit items for each key in nasa.near_earth_objects. Each item will be an array, with item[0] being the item's key (e.g., 2016-09-06) and item[1] being the item's value.
Using that idea, you can replace your DOM sink with something like:
nasa$.map(nasa => _.toPairs(nasa.near_earth_objects))
.flatMap(near_objects => Rx.Observable.from(near_objects))
.map(near_object => div([
h1(near_object[1].name)
]))
),