So I have this code in vue:
export default {
name: 'Test',
data() {
return {
test1: ['1', '2', '3'],
test2: [{
name: 'Hello'
}, {
name: 'Number two'
}, {
name: 'What ever'
}],
};
},
created() {
const first = [...this.test1];
first.forEach((elm, index) => first[index] = 'New');
console.log('first: ', first);
console.log('test1 in data', this.test1);
const second = [...this.test2];
second.forEach(elm => elm.name = 'New');
console.log('second: ', second);
console.log('test2 in data', this.test2);
},
}
After setting the value of each item of the array 'first' (that should be a copy without reference to the data 'test1' array) each item is equal to 'new'. The value of this.test1 doesn't change.
I did the same with test2. Copied and changed the value of each item to 'New'. But now the value of the data array 'test2' also has 'New' in every item.
I have no clue why this is like that. Any ideas?
Spread syntax creates a shallow copy. If your array has primitive types like numbers or strings, it won't update the original array. That's the case with test1. In the second case, only a new array is created. If you push or pop from the array, original array won't be updated. But, the objects are still pointing to their same place in memory. Updating them will update original array's objects as well.
You can use the spread syntax on the individual object to create a copy of the objects:
const second = this.test2.map(o => ({...o}))
You can also use JSON.parse and JSON.stringify. But, if the objects have any function properties, they'll be removed.
const second = JSON.parse(JSON.stringify(this.test2))
The reason it is like that is because you are having an array of Vue data values. So even though you are cloning the Array, you are also copying over each values 'getters' and 'setters' which have a reference to the original array. In order to remove the getters and setters you should do what d-h-e has suggested.
You could also do this.
const second = this.test2.map(() => { name: 'New' } );
console.log('second: ', second);
console.log('test2 in data', this.test2);
Try it with:
const second = JSON.parse(JSON.stringify(this.test2));
The copy method with spreadoperator or Array.from works only with simple arrays.
For deep copy use the method with JSON.parse and JSON.stringify.
Related
For the following code block:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach((item, i) => {
item = {
...item,
...changes
}
})
console.log(items) // items NOT reassigned with changes
items.forEach((item, i) => {
items[i] = {
...item,
...changes
}
});
console.log(items) // items reassigned with changes
Why does reassigning the values right on the element iteration not change the objects in the array?
item = {
...item,
...changes
}
but changing it by accessing it with the index does change the objects in the array?
items2[i] = {
...item,
...changes
}
And what is the best way to update objects in an array? Is items2[i] ideal?
Say no to param reassign!
This is a sort of a fundamental understanding of higher level languages like JavaScript.
Function parameters are temporary containers of a given value.
Hence any "reassigning" will not change the original value.
For example look at the example below.
let importantObject = {
hello: "world"
}
// We are just reassigning the function parameter
function tryUpdateObjectByParamReassign(parameter) {
parameter = {
...parameter,
updated: "object"
}
}
tryUpdateObjectByParamReassign(importantObject)
console.log("When tryUpdateObjectByParamReassign the object is not updated");
console.log(importantObject);
As you can see when you re-assign a parameter the original value will not be touched. There is even a nice Lint rule since this is a heavily bug prone area.
Mutation will work here, but ....
However if you "mutate" the variable this will work.
let importantObject = {
hello: "world"
}
// When we mutate the returned object since we are mutating the object the updates will be shown
function tryUpdateObjectByObjectMutation(parameter) {
parameter["updated"] = "object"
}
tryUpdateObjectByObjectMutation(importantObject)
console.log("When tryUpdateObjectByObjectMutation the object is updated");
console.log(importantObject);
So coming back to your code snippet. In a foreach loop what happens is a "function call" per each array item where the array item is passed in as a parameter. So similar to above what will work here is as mutation.
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach((item, i) => {
// Object assign just copies an object into another object
Object.assign(item, changes);
})
console.log(items)
But, it's better to avoid mutation!
It's better not mutate since this can lead to even more bugs. A better approach would be to use map and get a brand new collection of objects.
const items = [{
id: 1,
name: 'one'
},
{
id: 2,
name: 'two'
},
];
const changes = {
name: 'hello'
}
const updatedItems = items.map((item, i) => {
return {
...item,
...changes
}
})
console.log({
items
})
console.log({
updatedItems
})
As the MDN page for forEach says:
forEach() executes the callbackFn function once for each array
element; unlike map() or reduce() it always returns the value
undefined and is not chainable. The typical use case is to execute
side effects at the end of a chain.
Have a look here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
This means that although you did create new object for item, it was not returned as a value for that index of array. Unlike your second example, the first one is not changing original array, but just creates new objects and returns undefined. This is why your array is not modified.
I'd go with a classic Object.assign for this:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach( (item) => Object.assign(item,changes) )
console.log(items)
Properties in the target object are overwritten by properties in the sources if they have the same key. Later sources' properties overwrite earlier ones.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
The other approach you can take is to use map and create a new array based on the original data and the changes:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
const newItems = items.map((item) => {
...item,
...changes
})
console.log(newItems);
But if you need to modify the original array, it's either accessing the elements by index, or Object.assign. Attempting to assign the value directly using the = operator doesn't work because the item argument is passed to the callback by value not by reference - you're not updating the object the array is pointing at.
I have this two states:
stateOne: [
'marca01',
'marca02'
]
stateTwo: [
{
PRODUCTO:'hat',
PRICE:1499,
CATEGORY:'Men'
},
{
PRODUCTO:'Shirt',
PRICE:1233,
CATEGORY:'Woman'
}
]
And I'm using lodash to merge both in a third state, but as you see the first state is an array and the second one is an array of objects so I can't merge these two like I want to...
Something like this:
stateThree: [
{
PRODUCTO:'hat',
PRICE:1499,
CATEGORY:'Men',
MARK:'marca01'
},
{
PRODUCTO:'Shirt',
PRICE:1233,
CATEGORY:'Woman',
MARK:'marca02'
}
]
How can I get the desired result (is not necessary to use lodash)
Without lodash :
const state3 = state2.map((product, index) => ({
...product,
MARK: state1[index]
}));
Here .map returns a new array whose new values are returned by the anonymous function.
The spread operator ...product flattens the previous object properties in the new object. This syntaxic sugar can be replaced by Object.assign.
Then we add a new MARK property based on the other state with the same index.
I used the map method and the spread operator in order to prevent state mutation which can be harmful in React.
You could iterate over the second array, and add the properties.
let s1 = [
'marca01',
'marca02'
]
let s2 = [
{
PRODUCTO:'hat',
PRICE:1499,
CATEGORY:'Men'
},
{
PRODUCTO:'Shirt',
PRICE:1233,
CATEGORY:'Woman'
}]
s2.forEach((x,i) => x.MARK = s1[i])
console.log(s2)
You can do like this:
stateThree = stateTwo.map( (element, i) => ({...element, MARK: stateOne[i] } ))
Basically I have got an object, which will be multi-dimensional and the properties could be named anything and it could have many dimensions.
At some point I will need to append/splice a property within this object, from code which won't know it's position.
So, an example object:
let obj = {
response: {
locations: {
data: [
0: Object,
1: Object,
2: Object,
]
}
},
endpoint: null
}
I need to splice out data.locations.data[1], the only information I have is the below array and the index. Obviously I will know the first property will be response.
['locations','data']
index: 1
Edit:
My mistake, the data property has an array value not an object!
You can use Array#reduce() and pass in obj.response as the start value to get at the nested parent which based on the array shown would be obj.response.locations.data.
Then splice() the indexed item in that parent or do whatever other modifications are needed
const arr = ['locations','data'],
index= 1,
obj = {
response: {
locations: {
data: [{id:1},{id:2}, {id:3}]
}
},
endpoint: null
}
const targetArr = arr.reduce((a,c)=> (a[c]), obj.response);
targetArr.splice(index,1);
console.log(obj)
I have associative array.
It's a key(number) and value(object).
I need to keep state of this array same as it is I just need to update one object property.
Example of array:
5678: {OrderId: 1, Title: "Example 1", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
5679: {OrderId: 2, Title: "Example 2", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
I need to update Users array property.
I tried this but it doesn't work:
ordersAssociativeArray: {
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
}
This is data inside reducer.
What I did wrong how to fix this?
Something that might help.
When I inspect values in chrome I check previous value and value after execution of my code above:
Before:
ordersAssociativeArray:Array(22) > 5678: Order {OrderId: ...}
After:
ordersAssociativeArray: > 5678: {OrderId: ...}
Solution (code in my reducer)
let temp = Object.assign([], state.ordersAssociativeArray);
temp[action.key].Users = action.updatedUsers;
return {
...state,
ordersAssociativeArray: temp
}
So this code is working fine.
But I still don't understand why? So I have solution but would like if someone can explain me why this way is working and first not?
If it could help here how I put objects in this associative array initialy:
ordersAssociativeArray[someID] = someObject // this object is created by 'new Order(par1, par2 etc.)'
What you are doing is correct, as demonstrated by this fiddle. There may be problem somewhere else in your code.
Something that I would recommend for you is to separate your reducer into two functions, ordersReducer and orderReducer. This way you will avoid the excessive use of dots, which may be what caused you to doubt the correctness of your code.
For example, something like:
const ordersReducer = (state, action) => {
const order = state[action.key]
return {
...state,
[action.key]: orderReducer(order, action)
}
}
const orderReducer = (state, action) => {
return {
...state,
Users: action.updatedUsers
}
}
I hope you find your bug!
Update
In your solution you use let temp = Object.assign([], state.ordersAssociativeArray);. This is fine, but I thought you should know that it is sometimes preferable to use a {} even when you are indexing by numbers.
Arrays in javascript aren't great for representing normalized data, because if an id is missing the js array will still have an undefined entry at that index. For example,
const orders = []
array[5000] = 1 // now my array has 4999 undefined entries
If you use an object with integer keys, on the other hand, you get nice tightly packed entries.
const orders = {}
orders[5000] = 1 // { 5000: 1 } no undefined entries
Here is an article about normalizing state shape in redux. Notice how they migrate from using an array in the original example, to an object with keys like users1.
The problem can be that you're using array in the state but in the reducer you're putting as object. Try doing:
ordersAssociativeArray: [ //an array and not an object
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
]
It will put ordersAssociative array in your state and not an object.
let schools = [
{ name: "Yorktown"},
{ name: "Stratford" },
{ name: "Washington & Lee"},
{ name: "Wakefield"}
]
let updatedSchools = editName("Stratford", "HB Woodlawn", schools)
console.log( updatedSchools[1] ) // { name: "HB Woodlawn" }
const editName = (oldName, name, arr) =>
arr.map(item => {
if (item.name === oldName) {
// what is happening below!?
return {
...item,
name
}
} else {
return item
}
})
first of all, i'm sorry if this question might be easy for you, but i'm having trouble understanding how the return statement of the snippet works and would really appreciate help.
return { ...item, name }
So i would expect updatedSchool to be (even though it's invalid syntax):
[
{name: "Yorktown"},
{ name: "Yorktown", "HB Woodlawn"},
{ name: "Washington & Lee"},
{ name: "Wakefield"}
]
why does it produce { name: "HB Woodlawn" }?
Simply desugar expression step by step
{...item, name }
First {name} is shortcut for {name: name}
Then {...obj} is the same as Object.assign({}, obj)
Combining both gives Object.assign({}, obj, {name: name})
Given obj = {name: 'Stratford'} has only one property name it will simply create new object and replace name with a new one.
You can read about Object.assign here
return { // the spread operator assigns existing properties of item
...item, // to the new returned object
name // similar to return Object.assign(item, {name: name})
}
The rest parameter can work on objects as well as arrays in browsers that support it. If you want to understand the code, it's best to walk through it.
editSchools is a function that takes an oldName, a name, and an array. It returns the result of the mapping from array to a new array. Each element in the new array is determined by the callback function that map executes. If the item's name property is equal to the oldName, then a new object is created which will take its place, {...item, name}. This is where the confusion lies.
It does something weird. The new object recieves all the keys of the item object, and then it will define (or redefine) the name property to the value of name provided to editSchools.
So in essence, this code finds the objects that have a name key whose value is oldName and replaces it with an identical new object with a changed name property to the new name value.