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'm not sure what to name this, but basically I'm new to React and Redux and looking for a more correct/cleaner way to do this or just how to do this with my current set up.
I have a state that looks like this
--Character
---id
---name
---race
----id
----raceName
----traits
-----trait
------id
------name
------description
-----trait
------id
------name
------description
---classes
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
---traits
----trait
-----id
-----name
-----description
----trait
-----id
-----name
-----description
As you can see(hopefully) traits is an array of object TRAIT and classes is an array of object CLASS, in the end the whole state is quite a messy deal. I've read that I can somehow reference them by ID's but I'm not sure how if IDs are autogenerated.
So I kind of have two questions:
How do I simplify/flatten this structure if it even could be done?
If I can't simplify this structure is there anyway I can find a specific Trait with a specific ID without looping through all the objects that have property traits?
Yes. You can find Trait with a specific ID easily. Let know if this is what you are asking.
// Search in traits directly under Character.
const traitForId = this.state.Character.traits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
// Search in the list of traits under each Class.
const classTraits = this.state.Character.classes.map((class) => class.traits).flat();
const classTraitsForId = classTraits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
Find below recursive way to find a Trait irrespective of where it's present in the state.
function findTraitForId(state, specific_id){
if(state.traits){
const traitForId = state.traits.find((trait) => {
return trait.id == specific_id
});
if(traitForId)
return traitForId;
}
return Object.keys(state).filter((key) => key != 'traits').map((stateItem) => {
return findTraitForId(state[stateItem], specific_id);
}).flat();
}
Tried above function for the input
findTraitForId({'classes':[{traits: [{id: 1, name: "A"}, {id: 2, name: "AB"}]}, {traits: [{id: 3, name: "ABC"}, {id: 4, name: "ABCD"}]}], traits: [{id: 5, name: "ABCDE"}, {id: 6, name: "ABCDEF"}]}, 3)
which return
[{id: 3, name: "ABC"}]
I have an array of objects, and want to add a new object only if that object doesn't already exist in the array.
The objects in the array have 2 properties, name and imageURL and 2 objects are same only if their name is same, and thus I wish to compare only the name to check whether the object exists or not
How to implement this as a condition??
Since you've not mentioned the variables used. I'll assume 'arr' as the array and 'person' as the new object to be checked.
const arr = [{name: 'John', imageURL:'abc.com'},{name: 'Mike', imageURL:'xyz.com'}];
const person = {name: 'Jake', imageURL: 'hey.com'};
if (!arr.find(
element =>
element.name == person.name)
) {
arr.push(person);
};
If the names are not same, the person object won't be pushed into the array.
You can use Array.find
let newObj={ name:'X',imageURL:'..../'}
if(!array.find(x=> x.name == newObj.name))
array.push(newObj)
You need to check it using find or similar functions like this:
const arr = [{ name: 1 }, { name: 2 }];
function append(arr, newEl) {
if (!arr.find(el => el.name == newEl.name)) {
arr.push(newEl);
}
}
append(arr, { name: 2 }); // won't be added
console.log(arr);
append(arr, { name: 3 }); // will be added
console.log(arr);
I have an Object on sessionStorage for which I need to update values on user input. I am able to update at the root of the Object but not the values that are nested on a deeper level.
request('http://localhost:7474/graphql/', query).then(data => {...}
sessionStorage.setItem('queryData', JSON.stringify(data));
function update(value){
let prevData = JSON.parse(sessionStorage.getItem('queryData'));
Object.keys(value).forEach(function(val, key){
prevData[val] = value[val];
});
sessionStorage.setItem('queryData', JSON.stringify(prevData));
}
update({ maritalStatus: "single" });
So maritalStatus ends up been added and not replaced and I must replace the value:
Object: [,...]
0: {id: "x", maritalStatus: "married"} //want to replace this value here
maritalStatus: "single" // this is where the value is been written
Your data in storage is an Array. So the way you are updating it like prevData[val] = value[val]; is adding another property to the array with index of maritalStatus and value of "single". The object at index 0 is untouched.
My suggested fix is to also include the id in your update call. Then loop through the array in storage and look for the object with the matching id.
Once the id matches update that object, or log if no id matches are found.
let dataInStorage = [{
id: "x",
maritalStatus: "married"
}];
function update(updateObj) {
let prevData = dataInStorage;
let id = updateObj.id;
dataInStorage.forEach(function(data) {
if (data.id === id) {
Object.keys(updateObj).forEach(function(key, index) {
data[key] = updateObj[key];
});
} else {
console.log(`did not find object with id: ${id}`);
}
});
console.log(prevData)
//sessionStorage.setItem('queryData', JSON.stringify(prevData));
}
update({
id: "x",
maritalStatus: "single"
});
Here is what official docs said
updateIn(keyPath: Array<any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Array<any>, notSetValue: any, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, notSetValue: any, updater: (value: any) => any): List<T>
There is no way normal web developer (not functional programmer) would understand that!
I have pretty simple (for non-functional approach) case.
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.List.of(arr);
How can I update list where element with name third have its count set to 4?
The most appropriate case is to use both findIndex and update methods.
list = list.update(
list.findIndex(function(item) {
return item.get("name") === "third";
}), function(item) {
return item.set("count", 4);
}
);
P.S. It's not always possible to use Maps. E.g. if names are not unique and I want to update all items with the same names.
With .setIn() you can do the same:
let obj = fromJS({
elem: [
{id: 1, name: "first", count: 2},
{id: 2, name: "second", count: 1},
{id: 3, name: "third", count: 2},
{id: 4, name: "fourth", count: 1}
]
});
obj = obj.setIn(['elem', 3, 'count'], 4);
If we don’t know the index of the entry we want to update. It’s pretty easy to find it using .findIndex():
const indexOfListToUpdate = obj.get('elem').findIndex(listItem => {
return listItem.get('name') === 'third';
});
obj = obj.setIn(['elem', indexOfListingToUpdate, 'count'], 4);
Hope it helps!
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
Explanation
Updating Immutable.js collections always return new versions of those collections leaving the original unchanged. Because of that, we can't use JavaScript's list[2].count = 4 mutation syntax. Instead we need to call methods, much like we might do with Java collection classes.
Let's start with a simpler example: just the counts in a list.
var arr = [];
arr.push(2);
arr.push(1);
arr.push(2);
arr.push(1);
var counts = Immutable.List.of(arr);
Now if we wanted to update the 3rd item, a plain JS array might look like: counts[2] = 4. Since we can't use mutation, and need to call a method, instead we can use: counts.set(2, 4) - that means set the value 4 at the index 2.
Deep updates
The example you gave has nested data though. We can't just use set() on the initial collection.
Immutable.js collections have a family of methods with names ending with "In" which allow you to make deeper changes in a nested set. Most common updating methods have a related "In" method. For example for set there is setIn. Instead of accepting an index or a key as the first argument, these "In" methods accept a "key path". The key path is an array of indexes or keys that illustrates how to get to the value you wish to update.
In your example, you wanted to update the item in the list at index 2, and then the value at the key "count" within that item. So the key path would be [2, "count"]. The second parameter to the setIn method works just like set, it's the new value we want to put there, so:
list = list.setIn([2, "count"], 4)
Finding the right key path
Going one step further, you actually said you wanted to update the item where the name is "three" which is different than just the 3rd item. For example, maybe your list is not sorted, or perhaps there the item named "two" was removed earlier? That means first we need to make sure we actually know the correct key path! For this we can use the findIndex() method (which, by the way, works almost exactly like Array#findIndex).
Once we've found the index in the list which has the item we want to update, we can provide the key path to the value we wish to update:
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
NB: Set vs Update
The original question mentions the update methods rather than the set methods. I'll explain the second argument in that function (called updater), since it's different from set(). While the second argument to set() is the new value we want, the second argument to update() is a function which accepts the previous value and returns the new value we want. Then, updateIn() is the "In" variation of update() which accepts a key path.
Say for example we wanted a variation of your example that didn't just set the count to 4, but instead incremented the existing count, we could provide a function which adds one to the existing value:
var index = list.findIndex(item => item.name === "three")
list = list.updateIn([index, "count"], value => value + 1)
Here is what official docs said… updateIn
You don't need updateIn, which is for nested structures only. You are looking for the update method, which has a much simpler signature and documentation:
Returns a new List with an updated value at index with the return
value of calling updater with the existing value, or notSetValue if
index was not set.
update(index: number, updater: (value: T) => T): List<T>
update(index: number, notSetValue: T, updater: (value: T) => T): List<T>
which, as the Map::update docs suggest, is "equivalent to: list.set(index, updater(list.get(index, notSetValue)))".
where element with name "third"
That's not how lists work. You have to know the index of the element that you want to update, or you have to search for it.
How can I update list where element with name third have its count set to 4?
This should do it:
list = list.update(2, function(v) {
return {id: v.id, name: v.name, count: 4};
});
Use .map()
list = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.fromJS(arr);
var newList = list.map(function(item) {
if(item.get("name") === "third") {
return item.set("count", 4);
} else {
return item;
}
});
console.log('newList', newList.toJS());
// More succinctly, using ES2015:
var newList2 = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
console.log('newList2', newList2.toJS());
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
I really like this approach from the thomastuts website:
const book = fromJS({
title: 'Harry Potter & The Goblet of Fire',
isbn: '0439139600',
series: 'Harry Potter',
author: {
firstName: 'J.K.',
lastName: 'Rowling'
},
genres: [
'Crime',
'Fiction',
'Adventure',
],
storeListings: [
{storeId: 'amazon', price: 7.95},
{storeId: 'barnesnoble', price: 7.95},
{storeId: 'biblio', price: 4.99},
{storeId: 'bookdepository', price: 11.88},
]
});
const indexOfListingToUpdate = book.get('storeListings').findIndex(listing => {
return listing.get('storeId') === 'amazon';
});
const updatedBookState = book.setIn(['storeListings', indexOfListingToUpdate, 'price'], 6.80);
return state.set('book', updatedBookState);
You can use map:
list = list.map((item) => {
return item.get("name") === "third" ? item.set("count", 4) : item;
});
But this will iterate over the entire collection.