I have a tree of nodes, each time user "expands" a node, an http request is invoked to get it's children.
I'm looking for an RXJS pipeline to recursively expand all tree nodes and emit the expanded tree.
( StackBlitz demo )
The way I do it now, is to mutate the source and output the mutated object when done.
Is there a way for me to simplify this hell ? possibly without mutating source .
// 'ROOT' nodes
const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];
// map the root nodes to a recursive function that mutate node and returns its children observables
const getChildrenOfRoot$ = rootNodes.map(node => getChildren(node));
// call the observables, in parallels, when done, print the *** mutated *** source.
forkJoin(...getChildrenOfRoot$).subscribe(() => console.log(rootNodes));
function getChildren(node: Node): Observable<Node[]> {
return getChildrenFromServer(node.id).pipe(
// if no children returned, don't continue
filter((children: Node[]) => !!(children?.length)),
// mutate argument's .children property with the returned children array.
tap((children: Node[]) => (node.children = children)),
mergeMap((children: Node[]) => {
// mape children to observables returning either their children, or, an empty array, based on 'parent' property.
const getChildrenOfChildren$ = children.map(c => (c.parent ? getChildren(c) : of([])));
return forkJoin(...getChildrenOfChildren$);
})
);
}
TLDR;
StackBlitz app.
Detailed explanation
Since we're dealing with recursion, I thought it would be easier if we started from a base case.
Let's see what we have initially:
const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];
Assuming these are the only parents in this tree(i.e their children will all be leaf nodes), our problem becomes easier: for each node in the above array, we must fetch its children and add a new property to their nodes, called children.
For this, let's create the processNodes children:
function processNodes (nodes: Node[]): Observable<Node[]> {
return nodes.length
? forkJoin(
nodes.map(node => node.parent ? processNode(node) : of(node))
)
: of([]);
}
Note that we're also considering the case where the nodes array is empty, so, in that case, we simply return an empty array(of([])). Let's now focus on this part:
forkJoin(
nodes.map(node => node.parent ? processNode(node) : of(node))
)
If the argument of processNodes was:
[{ id: 0, parent: true }, { id: 1000, parent: true }, { id: 7, parent: false }]
then the expected output would be:
[
{ id: 0, parent: true, children: [/* ... */] },
{ id: 1000, parent: true, children: [/* ... */] },
{ id: 7, parent: false }
]
So, in case a node is a parent, then we have separated logic encapsulated in the processNode function:
function processNode (node: Node) {
return getChildrenFromServer(node.id).pipe(
mergeMap((children: Node[]) => processNodes(children)),
map((children: Node[]) => ({ ...node, ...children.length && { children } })),
);
}
and here is where the recursion happens. Given the children nodes of some parent node, we repeat the process and at the end we add the children property to the parent node.
So, to put all the parts together:
processNodes(rootNodes).subscribe(console.log)
function processNodes (nodes: Node[]): Observable<Node[]> {
return nodes.length
? forkJoin(
nodes.map(node => node.parent ? processNode(node) : of(node))
)
: of([]);
}
function processNode (node: Node) {
return getChildrenFromServer(node.id).pipe(
mergeMap((children: Node[]) => processNodes(children)),
map((children: Node[]) => ({ ...node, ...children.length && { children } })),
);
}
Related
Hellow,
I have below Json structure, which is provided as a payload in the UpdateOrders action.
In the effect, I would like to iterate over the reservations and orders, call the this.orderApiService.updateOrder service and dispatch a UpdateOrderProgress action. In the UpdateOrderProgress action I would like to provide the numberOfReservationsUpdated and the totalReservations
const reservationOrders = [
{
reservationNumber: '22763883',
orders: [
{
orderId: 'O12341',
amount: 25
},
{
orderId: 'O45321',
amount: 50
}
]
},
{
reservationNumber: '42345719',
orders: [
{
orderId: 'O12343',
amount: 75
}
]
}
];
I have the following effect to achieve this, but unfortunately, this effect does not work and throws an exception.
#Effect()
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
return combineLatest(action.reservationOrders.map((x, index) => {
const totalReservations = action.reservationOrders.length;
const numberOfReservationsUpdated = index + 1;
return combineLatest(x.orders.map((order) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => [new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)]),
catchError((message: string) => of(console.info(message))),
);
}))
}))
})
);
How can I achieve this? Which RxJs operators am I missing?
Instead of using combineLatest, you may switch to using a combination of merge and mergeMap to acheive the effect you're looking for.
Below is a representation of your problem statement -
An action triggers an observable
This needs to trigger multiple observables
Each of those observables need to then trigger some
action (UPDATE_ACTION)
One way to achieve this is as follows -
const subj = new Subject<number[]>();
const getData$ = (index) => {
return of({
index,
value: 'Some value for ' + index,
}).pipe(delay(index*1000));
};
const source = subj.pipe(
filter((x) => !!x),
exhaustMap((records: number[]) => {
const dataRequests = records.map((r) => getData$(r));
return merge(dataRequests);
}),
mergeMap((obs) => obs)
);
source.subscribe(console.log);
subj.next([3,1,1,4]); // Each of the value in array simulates a call to an endpoint that'll take i*1000 ms to complete
// OUTPUT -
// {index: 1, value: "Some value for 1"}
// {index: 1, value: "Some value for 1"}
// {index: 3, value: "Some value for 3"}
// {index: 4, value: "Some value for 4"}
Given the above explaination, your code needs to be changed to something like -
const getOrderRequest$ = (order: OrderUpdateRequest, numberOfReservationsUpdated, totalReservations) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)),
catchError((message: string) => of(console.info(message))),
);
}
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
const reservationOrders = action.reservationOrders;
const totalLen = reservationOrders.length
const allRequests = []
reservationOrders.forEach((r, index) => {
r.orders.forEach(order => {
const req = getOrderRequest$(order, index + 1, totalLen);
allRequests.push(req);
});
});
return merge(allRequests)
}),
mergeMap(obs=> obs)
);
Side Note - While the nested observables in your example may work, there are chances that you'll be seeing wrong results due to inherent nature of http calls taking unknown amount of time to complete.
Meaning, the way you've written it, there are chances that you can see in some cases that numberOfReservationsUpdated as not an exact indicative of actual number of reservations updated.
A better approach would be to handle the state information in your reducer. Basically, pass the reservationNumber in the UPDATE action payload and let the reducer decide how many requests are pending completion. This will be an accurate representation of the state of the system. Also, it will simplify your logic in #effect to a single nested observable rather than multiple nesting.
I am listening to an observable an after the first emit with all the objects, I would to get only the object that changed. So if I have:
[{name: 'Mark'},{name: 'Joe'}]
and then a name change I only get the object that changed. So if the object becomes:
[{name: 'Jean Mark'},{name: 'Joe'}]
I only get
[{name: 'Jean Mark'}]
Your Observable emits arrays and you want to know the difference between the currently emitted array and the previous one. Tracking array state changes has more to do with how to compare arrays or objects than with Observables.
If you want to track changes within an Observable it really comes down to comparing a previous with a current value. The logic you want to use here is up to you. e.g. you have to think about how to distinguish between a 'modified' value and newly 'added' value in an array?
Check out these questions to get you started:
How to get the difference between two arrays in JavaScript?
Comparing Arrays of Objects in JavaScript
How to determine equality for two JavaScript objects?
You can compare the current value cv to the previous one pv in an Observable by using pairwise. Here is a how it could look like.
const source = of(
[{ name: "Mark", p: 2 }, { name: "Joe", p: 3 }],
[{ name: "Jean Mark", p: 2 }, { name: "Joe", p: 3 }],
[{ name: "Jean Mark", p: 1 }, { name: "Joe", p: 3 }, { name: 'Alice' }],
[{ name: "Jean Mark", p: 1 }, { name: "Joe", p: 3 }],
[{ name: "Jean Mark", p: 1 }, { name: "Joe", p: 4 }],
[{ name: "Jean Mark", p: 1 }, { name: "Joe", p: 4 }]
);
// compare two objects
const objectsEqual = (o1, o2) =>
typeof o1 === "object" && Object.keys(o1).length > 0
? Object.keys(o1).length === Object.keys(o2).length &&
Object.keys(o1).every(p => objectsEqual(o1[p], o2[p]))
: o1 === o2;
// compare two arrays
// REPLACE this function with YOUR OWN LOGIC to get your desired output !!!
const difference = (prev, curr) => ({
added: curr.filter(o1 => !prev.some(o2 => objectsEqual(o1, o2))),
removed: prev.filter(o1 => !curr.some(o2 => objectsEqual(o1, o2)))
})
source.pipe(
startWith([]), // used so that pairwise emits the first value immediately
pairwise(), // emit previous and current value
map(([pv, cv]) => difference(pv, cv)) // map to difference between pv and cv
).subscribe(console.log);
https://stackblitz.com/edit/rxjs-m9ngjy?file=index.ts
You can watch an array (index value/add/remove) with javascript proxy, but that doesn't watch for object change in the array.
const handler = {
set: function(target, property, value, receiver){
console.log('setting ' + property + ' for ' + target + ' with value ' + value);
target[property] = value;
return true;
}
}
const arr=[{name: 'Mark'},{name: 'Joe'}];
const proxy = new Proxy(arr, handler);
// will log
proxy[0]="hello"
// won't log
proxy[0].name="ben"
if you also want to watch for object change then you need to either use proxy for every object added, or create your to be added object with Object.defineProperty()
and add your setter
There is also an existing library that watch for both object and array change, and it also use proxy
https://github.com/ElliotNB/observable-slim/
Hi all i am new to react hooks.Please explain what is the meaning of state.items in DELETE_ITEM case.Is this a single object if yes then how.
let initialState = {
items: [
{
name: 'A',
age: 23
},
{
name: 'B',
age: 20
},
{
name: 'C',
age: 29
}
]
}
const userReducer = (state = initialState, action) => {
switch(action.type){
case DELETE_ITEM:
return {
...state,
items: state.items.filter((item, index) => index !== action.payload)
}
}
}
state.items is the items object from your state. It starts off as described in initialState. That's the initial value. Then, in the reducer, subsequent actions such as DELETE_ITEM might alter that value.
The current state of that items value is what you have in state.items. Hence, the name. It's not a single object, it's the entire items array.
Code:
const Rx = require('rxjs')
const data = [
{ name: 'Zachary', age: 21 },
{ name: 'John', age: 20 },
{ name: 'Louise', age: 14 },
{ name: 'Borg', age: 15 }
]
const dataSubj$ = new Rx.Subject()
function getDataStream() {
return dataSubj$.asObservable().startWith(data);
}
getDataStream()
.mergeMap(Rx.Observable.from)
.scan((arr, person) => {
arr.push(person)
return arr
}, [])
.subscribe(val => console.log('val: ', val));
Using .reduce(...) instead of .scan(...) returns an empty array and nothing is printed. The observer of dataSub$ should receive an array.
Why does scan allow elements of data to pass through, but reduce does not?
Note: I am using mergeMap because I will filter the elements of the array before reducing them back into a single array.
scan emits the accumulated value on every source item.
reduce emits only the last accumulated value. It waits until the source Observable is completed and only then emits the accumulated value.
In your case the source Observable, which relies on a subject, never completes. Thus, the reduce would never emit any value.
You may want to apply the reduce on the inner Observable of the mergeMap. For each array, the inner Observable would complete when all the array items are emitted:
const data = [
{ name: 'Zachary', age: 21 },
{ name: 'John', age: 20 },
{ name: 'Louise', age: 14 },
{ name: 'Borg', age: 15 }
]
const dataSubj$ = new Rx.Subject()
function getDataStream() {
return dataSubj$.asObservable().startWith(data);
}
getDataStream()
.mergeMap(arr => Rx.Observable.from(arr)
.reduce((agg, person) => {
agg.push(person)
return agg
}, [])
)
.subscribe(val => console.log('val: ', val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
I have the following hierarchical data structure:
const tree = [
{
node: true,
key: 1,
children: [
{
node: true,
key: 2,
children: [
{ leaf: true, key: 1 },
{ leaf: true, key: 2 }
//...
]
}
]
},
///
];
I'd like to filter only items that marked as leaf. Then, I need to remove nodes, that have no leaves.
For example, if filter criterion is key > 2, the result should be empty.
I've tried to use a recursive function, but have no positive result.