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