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/
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.
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.
I am trying to return single string once map the whole array object.
Here it's below code that combine my custom array input to string but it's emit each value alone instead map into single block of string(concatenation of string).
const exampleInfo: GithubInfo = {
name: "Hello",
login: "Hello1",
description: "TypeScript dev",
repos: [{ project: "ts", star: 5 }, { project: "js", star: 5 }]
};
const repos = from(gitHubInfo["repos"]);
const reposeDetial = repos.pipe(
map(val => `${val.project},${val.star}`))
.subscribe(val => {
console.log(val); // emit `ts,5` ,`js,5` instead in single block `ts,5,js,5`
});
If you want to concat your emitted data at the end of your operators chain, you can use reduce. It will accumulate all your stream into one single value:
const exampleInfo: GithubInfo = {
name: "Hello",
login: "Hello1",
description: "TypeScript dev",
repos: [{ project: "ts", star: 5 }, { project: "js", star: 5 }]
};
const repos = from(gitHubInfo["repos"]);
const reposeDetial = repos.pipe(
map(val => `${val.project},${val.star}`)),
reduce((acc, value) => acc.concat(value), '')
).subscribe(val => {
console.log(val); // emit `ts,5,js,5`
});
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 trouble understanding the proper way to call attributes of a child in a nested object model, and use them to compute a new property. Here is what I'm trying so far:
Monitor.ProcessController = Ember.ObjectController.extend({
nbUsers: function() {
var total = 0;
this.get('containers').then(function(containers) {
containers.forEach(function(container, index){
total += container.get('nbUsers');
});
});
return total;
}.property('containers.#each.nbUsers')
});
Monitor.ContainerController = Ember.ObjectController.extend();
Monitor.Process = DS.Model.extend({
containers: DS.hasMany('container', {async : true}),
name: DS.attr('string')
});
Monitor.Container = DS.Model.extend({
process: DS.belongsTo('process'),
name: DS.attr('string'),
nbUsers: DS.attr('integer')
});
// Test data
Monitor.Process.FIXTURES = [
{
id: 1,
name: 'Mumble',
containers: [1,2]
},
{
id: 2,
name: 'EVE',
containers: [3]
}
];
Monitor.Container.FIXTURES = [
{
id: 1,
name: 'First',
process: 1,
nbUsers: 1
},
{
id: 2,
name: 'Second',
process: 1,
nbUsers: 1
},
{
id: 3,
name: 'Unique',
process: 2,
nbUsers: 1
}
];
So a Process has multiple child Containers, and I would like to compute the number of total users for each process, based on the containers data, dynamically. Sounds trivial, is apparently not.
{{nbUsers}} keeps returning "0".
Any pointers on what I'm doing wrong?
You're property is asynchronous, so you return 0 before you ever update total, and since it's not a reference being passed back, it's not like it will update the value that's already been returned. So the property total gets updated, but not the value that was returned to the computed property.
The easiest way to change it, is to make it an observer, and have it update the property.
Monitor.ProcessController = Ember.ObjectController.extend({
nbUsers: 0,
nbUsersWatcher: function() {
var self = this;
this.get('containers').then(function(containers) {
var total = 0;
containers.forEach(function(container, index){
total += container.get('nbUsers');
});
self.set('nbUsers', total);
});
}.observes('containers.#each.nbUsers')
});
You might be able to use a computed as well, but I'd have to test that before I tell you that's possible.