Starting from an object like this:
const business = {
id: "1a2b3c",
accounts: [
{
name: "Pizza Express",
stores: [
{ id: "5", webSite: "www.pizza-ny.com" },
{ id: "6", webSite: "www.pizza-la.com" }
]
}
]
};
Expected output:
A stream of Stores (not an array of Stores)
My attempt:
return business.pipe(
pluck("accounts"),
flatMap(val => val),
pluck("stores"),
flatMap(val => val)
);
This works but, for me, the operators seem like repetitive and I'm wondering if I can simplify them. Also tried with the common map but I can't get it.
For example:
return business.pipe(
pluck("accounts"),
map(val => val.stores)
);
I don't understand why this returns undefined. I get an array of accounts and then projecting the stores property of each one... I guess I'm misunderstanding something.
Do you see a simpler or more elegant approach?
There will be some duplication in your operators since you are reaching into nested objects for nested arrays. There is probably a way to write a nice custom operator (maybe I'll look into that if I have time), but I think the simplest way is to use just the mergeMap (same thing as flatMap).
It will look something like this (edited to filter out accounts without stores – credit to Picci's comment)
import { of } from "rxjs";
import { mergeMap, filter } from "rxjs/operators";
of(business)
.pipe(
mergeMap(b => b.accounts),
/* filter out accounts that don't have stores */
filter(a => !!a.stores),
mergeMap(a => a.stores)
)
.subscribe(console.log);
/** console output:
*
* { id: "5", webSite: "www.pizza-ny.com" }
* { id: "6", webSite: "www.pizza-la.com" }
*/
Here is the stackblitz: https://stackblitz.com/edit/rxjs-njvj7q?devtoolsheight=33&file=index.ts
Side note: you could even use switchMap() instead of mergeMap(). Since you are dealing with observables that complete, there isn't much difference between which one you use.
EDIT (explanation for map differences)
The reason your map() isn't working is because you are trying to access .stores on an array, not an object:
return business.pipe(
pluck("accounts"), // accounts is an array
map(val => val.stores) // `accounts[0].stores` would return a value for you
);
map works differently between Array.prototype.map() and RxJS map() because RxJS has a different implementation.
In plain JavaScript map() is a function on an array and will return a new array. The key there is it is always array based. From the docs (emphasizes added):
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
In RxJS map() is an operator that is applied to any value inside an Observable. It is not guaranteed to be an array so it does not iterate over the value. RxJS map() is taking the value inside of an Observable and "mapping" it to a new structure/value. From the docs (emphasizes added):
Applies a given project function to each value emitted by the source Observable, and emits the resulting values as an Observable.
If I understand right, what you want to achieve is to create an Observable that emits, as a stream, each store of all the accounts.
So one way could be this
business.pipe( // business is assumed to be an Observable that emits the business Object you describe in your question
map(b => b.accounts),
mergeMap((accounts) => accounts),
mergeMap(account => account.stores)
)
At the end of the day it is not much different from your solution.
The reason why your last snippet does not work is explained in the comments I have added to your code below.
return business.pipe(
// With `pluck(accounts)` you extract the `accounts` property of the `business` object.
// Such property is an `array` of accounts.
pluck("accounts"),
// Now you want to map the `stores` property of `val`, but `val` is an array
// and an array has not `stores` property, so you get `undefined`
map(val => val.stores)
);
Related
I am working with a VueX store at the moment, and I have 1 mutation that does a push to entire array. Everything works currently, but want to know if I can make this code simpler, less lines / more optimized at the same time.
Side note, these are objects stored in an array.
PUSH_TO_ALL: (state, data) => {
const found = state.all.find((one, i) => {
if (one.slug === data.slug) {
state.all[i] = data // modify current state
return true
}
return false
})
if (!found) {
state.all.push(data) // add to the state
}
}
My first thought is that if you compare the slug in data to that in every object in the array, it must be unique (otherwise you would replace multiple objects in the find).
This means that you can almost certainly make things a lot faster and a lot simpler if you switch from having the 'root' of state be an array, to using an object instead, indexed by slug.
Then your code would switch to being something like:
PUSH_TO_ALL: (state, data) => {
state.all[data.slug] = data
This has 2 advantages - it is much simpler and faster to modify state, since you don't need to walk all of state checking if the object already exists. And secondly there's no need for separate code to distinguish between adding a new object, and replacing it if it already exists.
If for some reason you have to store state as an array, I would use a different part of state to maintain an object which tracks slug to array index. Then your code would be something like:
PUSH_TO_ALL: (state, data) => {
if (state.map[data.slug]) {
state.all[state.map[data.slug]] = data
} else {
// Push returns length of array - index is len-1
state.map[data.slug] = state.all.push(data) - 1
}
Note - in Vue2 you may need to use Vue.set() to update nested objects, since otherwise the code may not react to these changes. Vue3 no longer has this limitation.
You could use Array.findIndex instead of Array.find, and pair it with a ternary to make the trivial logic more concise (though not necessarily clearer).
const mutations = {
PUSH_TO_ALL: (state, data) => {
const indexOfMatchingSlug = state.all.findIndex(one => one.slug === data.slug);
state.all[indexOfMatchingSlug] = data ? indexOfMatchingSlug > -1 : state.all.push(data);
}
}
Array.findIndex documentation
JavaScript ternary operator documentation
Currently, I am working on a pretty complicated set of queries to firestore.
I am trying, all at once, to populate an array full of references to other documents, and then read those documents and put the information in an array.
More specifically to this example, I have 4 references in one collection. I want to get those references, and then using those references, query 4 documents and populate an array with the information.
The order is as follows: do a query for all of the documents in the tags subcollection, which is handled by the function below:
getTagsOnPage(workspaceId: string, pageId: string) {
// get all of the references in the tags sub-collection, and puts them in an array
// get all id's, do not get data
return this.afs
.collection("users")
.doc(`${auth().currentUser.uid}`)
.collection<Workspace>("workspaces")
.doc(`${workspaceId}`)
.collection<Page>("pages")
.doc(`${pageId}`)
.collection("tags")
.snapshotChanges()
.pipe(
map((actions) => {
return actions.map((a) => {
const ref = a.payload.doc.get("reference");
return ref; // return an array of (id: reference) key-value pairs
});
})
);
}
This works fine with the following code performing the subscription:
this.pageService
.getTagsOnPage(this.workspaceId, this.currentPage.id)
.subscribe((data) => {
temp = data;
});
data is as follows, via the console:
(3) ["/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1…1tSnPI5GJrniY82vZL/localTags/1p5Tscwn14PyK6zRaFHX", "/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1tSnPI5GJrniY82vZL/localTags/lFKoB0jngmtnALut2QS2", "/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1tSnPI5GJrniY82vZL/localTags/r6sf2SX6v87arU2rKsD5"]
Now, to perform the next set of data reads is where my confusion begins.
My initial approach was to try a for loop (for the length of this array), but this would involve iterating performing a number of nested subscriptions, which I do not think is possible in this sense.
I am fairly new to rxjs, and have only used the map and switchMap operators. In this case, I am imagining I would use something like mergeMap and/or flatMap, but frankly, I am not sure how to make this work in this case. Also, dealing with the for loop where I need to grab documents based on the array of documentReferences I get is also throwing me for a loop.
This is my current implementation, which is all over the place; I hope the feel for what I am trying to do is there. Basically, get the array of references via getTagsOnPage, wait for the observable to end, use switchMap to take the data array and loop over it; ideally, subscribe to each ref and add to tagData, and then return that:
let tagData;
this.pageService.getTagsOnPage(this.workspaceId, this.currentPage.id).pipe(
switchMap((data) => {
let references = data;
for (let j = 0; j < references.length; j++) {
let ref = this.afs.doc(`${references[j]}`);
ref.snapshotChanges().pipe(
map((actions) => {
const data = actions.payload.data();
tagData.push(data);
})
);
// push the data (different data var)
}
})
);
return tagData;
Messy, I know, but I think once I know the right operators to use this will make a lot more sense.
Also, atm this returns an empty array. There is an error for when I use switchMap that says the following:
Argument of type '(data: any[]) => void' is not assignable to parameter of type '(value: any[], index: number) => ObservableInput<any>'.
Type 'void' is not assignable to type 'ObservableInput<any>'.
Thank you for any help!
The reason for your error using switchMap is because you are not returning an Observable.
When using any of the "Higher Order Mapping Operators" (switchMap, concatMap, mergeMap, etc), the provided function must return an observable. As the error states: "Type void is not assignable to type ObservableInput<any>", you aren't returning anything (void).
The next thing that isn't quite right is that within your loop, you reference ref.snapshotChanges().pipe(), but you never subscribe to it. As you may know, observables are lazy and won't fire unless there is a subscriber.
As long as you return an observable inside of switchMap(), it will automatically subscribe to it and emit its values.
Let's think about this a little bit differently; instead of looping over the results of your first call, executing them, then pushing the values into an array. We can instead take the results and turn them into an observable stream that emits all the results of their individual calls, and combines them into an array for you. But... with a subtle difference: I'm suggesting not to have a separate tagData array outside the stream, but to have your observable return the tagData form that you need as an Observable<TagData[]>.
I think something like this will work for you:
tagData$ = this.pageService.getTagsOnPage(this.workspaceId, this.currentPage.id).pipe(
switchMap(references => from(references)),
mergeMap(ref => this.afs.doc(ref).snapshotChanges()),
map(actions => actions.payload.data()),
scan((allTagData, tagData) => [...allTagData, tagData], [])
})
);
Let's break that down!
We use from to create an observable from your array, that emits each "reference" one at a time.
mergeMap will subscribe to the observable we are creating and emit its emissions
map simply transforms the value into your desired shape
scan accumulates your emissions for you and emits on each change. if you don't want to emit until all calls come back, use reduce() instead
Now, you can simply do: tagData$.subscribe() and do what you want with your resulting array of data. Or you could even use the async pipe in your template rather than subscribing in your component.
I am filtering an array of JSON objects, I want to return the found object based on the passed in id argument.
getClient(id) {
return this.http.get<any>(' http://localhost:3000/clients')
.pipe(
filter(client => client.idNumber === id)
);
}
The observer to the above is not triggered and does not receive any data.
getClient(id)
.subscribe(
(data) => {
// nothing here
}
)
The below implementation of getClient achieves what I want.
getClient(id) {
return this.http.get<any>(' http://localhost:3000/clients')
.pipe(
map(clients => clients.find(client => client.idNumber === id))
);
}
I would like to understand, am I using the filter operator the wrong way?
And how is my usage different from the one here
Correct, you are using the rxjs filter operator the wrong way.
Per the docs:
Filter items emitted by the source Observable by only emitting those that satisfy a specified predicate.
In your example, the client value that is getting sent to filter is the array of clients that your http request returned. Then, you are saying "don't emit anything unless client (which is actually an array and has no "idNumber" property is equal to id, which will always be false because unefined !== number
If you want to modify the , you need to use the map operator. The map operator takes what the observable emits, lets you do something (like filter or find in the array), and then return that modified data to subscribers.
Also, if you type your responses property, TypeScript will warn you about this. Instead of this.http.get<any> use this.http.get<Client[]> or some more appropriate type. You would be able to see at compile time that Client[] has no property idNumber.
I am trying to delete a property name from the array of object, it's working properly using filter API,
const users = [
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
];
const myProp = users.filter(function (props) {
delete props.name;
return true;
});
console.table(myProp);
const myProp2 = users.reduce((people, user) => {
console.log(people);
console.log(user);
delete user.name;
return people;
}, []);
console.log(myProp2);
The same example before I am trying complete using reduce API, However, it's not working as expected.
It's not working because your not pushing to the previous element (you are always returning the empty array). You need to change it to:
const myProp2 = users.reduce((people, user) => {
delete user.name;
people.push(user)
return people;
}, []);
Please note that is not the intended use for reduce though - map is the operation you are looking for:
const myProp2 = users.map(u=> ({age: u.age}));
You actually want to use map for this, because you are selecting a transormation of the data into a new object (similar to Select in SQL or LINQ)
const myProps = users.map(u=> ({age: u.age}))
Also although the filter method worked, this is actually abuse of the filter method. The filter method is supposed to remove elements from the array depending on a condition. Your method worked because you returned true (which removed no elements) but you modified the current value on each iteration.
This is bad practice because you will confuse the next person to look at your code, they will wonder why you used filter as a method to transform the data rather than map.
Also don't use reduce because reduce is an aggregation function intended to perform aggregate functions on objects. Since the number of elements you are returning will be the same, map is better for this.
Reduce would be better suited for if you wanted to find out the average,max,min,median age or the most popular name etc...
I am working on an Angular 9, RxJS 6 app and have a question regarding the different outcomes of piping subject values and doing unit conversion in that pipe.
Please have a look at this stackblitz. There, inside the backend.service.ts file, an observable is created that does some "unit conversion" and returns everything that is emmitted to the _commodities Subject. If you look at the convertCommodityUnits function, please notice that I commented out the working example and instead have the way I solved it initially.
My question: When you use the unsubscribe buttons on the screen and subscribe again, when using the "conversion solution" that just overrides the object without making a copy, the values in the HTML are converted multiple times, so the pipe does not use the original data that the subject provides. If you use the other code, so creating a clone of the commodity object inside convertCommodityUnits, it works like expected.
Now, I don't understand why the two ways of converting the data behave so differently. I get that one manipulates the data directly, because js does Call by sharing and one returns a new object. But the object that is passed to the convertCommodityUnits function is created by the array.prototype.map function, so it should not overwrite anything, right? I expect that RxJS uses the original, last data that was emitted to the subject to pass into the pipe/map operators, but that does not seem to be the case in the example, right?
How/Why are the values converted multiple times here?
This is more or less a follow-up question on this: Angular/RxJS update piped subject manually (even if no data changed), "unit conversion in rxjs pipe", so it's the same setup.
When you're using map you got a new reference for the array. But you don't get new objects in the newly generated array (shallow copy of the array), so you're mutating the data inside the element.
In the destructuring solution, because you have only primitive types in each object in the array, you kind of generate completely brand new elements to your array each time the conversion method is called (this is important: not only a new array but also new elements in the array => you have performed a deep copy of the array). So you don't accumulate successively the values in each subscription.
It doesn't mean that the 1-level destructuring solution like you used in the provided stackblitz demo will work in all cases. I've seen this mistake being made a lot out there, particularly in redux pattern frameworks that need you to not mutate the stored data, like ngrx, ngxs etc. If you had complex objects in your array, the 1-level destructuring would've kept untouched all the embedded objects in each element of the array. I think it's easier to describe this behavior with examples:
const obj1 = {a: 1};
const array = [{b: 2, obj: obj1}];
// after every newArray assignment in the below code,
// console.log(newArray === array) prints false to the console
let newArray = [...array];
console.log(array[0] === newArray[0]); // true
newArray = array.map(item => item);
console.log(array[0] === newArray[0]); // true
newArray = array.map(item => ({...item}));
console.log(array[0] === newArray[0]); // false
console.log(array[0].obj === newArray[0].obj); // true
newArray = array.map(item => ({
...item,
obj: {...item.obj}
}));
console.log(array[0] === newArray[0]); // false
console.log(array[0].obj === newArray[0].obj); // false