I am trying to return a new columns object with updated tabs array and replace this object the existing one in my datasource. Here's the relevant part. Below, uses spread, however, it adds my object to the end of the columns. How can I make it replace the existing column?
Thanks!
newState = {
columns: [
{ id: column.id, title: column.title, tabs: removedTabs },
...state.columns
],
columnOrder: ["chromeTabs", ...state.columnOrder]
};
codesandbox link
newState = {
columns: [
...state.columns.filter(item => item.id !== column.id),
{ id: column.id, title: column.title, tabs: removedTabs }
],
columnOrder: [...state.columnOrder.filter(item => item !== 'chromeTabs'), "chromeTabs"]
};
return a filtred array and your new item, should do what you expect, using spread operator to replace existing item only work on object (because key are unique) not array.
const obj = {
cool: "is it cool ?"
};
console.log({ ...obj, cool: "definetly" });
Related
I have a possible infinite category tree and I would like to add, update or remove categories at any level with setState in react. I know this is possible with recursion but I don't have enough experience to manage this problem on my own. Here is how the data could possible look like:
const categories = [
{
id: "1",
name: "category1",
subCategories: [
{
id: "sub1",
name: "subcategory1",
subCategories: [
{ id: "subsub1", name: "subsubcategory1", subCategories: [] },
{ id: "subsub2", name: "subsubcategory2", subCategories: [] }
]
},
{ id: "sub2", name: "subcategory2", subCategories: [] }
]
},
{
id: "2",
name: "category2",
subCategories: []
}
]
Considering that your top level categories object is an object and not an array, the add and remove function could be the following (same pattern for update)
function add (tree, newCategory, parentId) {
if(tree.id === parentId)
return {
...tree,
subCategories: tree.subCategories.concat(newCategory)
}
return {
...tree,
subCategories: tree.subCategories.map(c => add(c, newCategory, parentId))
}
}
function remove (tree, idToRemove) {
if(tree.subCategories.map(c => c.id).includes(idToRemove))
return {
...tree,
subCategories: tree.subCategories.filter(c => c.id !== idToRemove)
}
return {
...tree,
subCategories: tree.subCategories.map(c => remove(c, idToRemove))
}
}
Prologue
To update a nested property in an immutable way, you need to copy or perform immutable operations on all its parents.
Setting a property on a nested object:
return {
...parent,
person: {
...parent.person,
name: 'New Name'
}
}
Arrays: You may pre-clone the array, or use a combination of .slice(), .map(), .filter() and the concatenation operator (...); Warning: .splice mutates the array.
(this can be a long topic, so I just gave a veryfast overview)
immer
As this can quickly get very ugly on objects with deep nesting, using the immer lib quite becomes a must at some point. Immer "creates new immutable state from mutations".
const newState = immer(oldState, draft => {
draft[1].subcategories[3].subcategories[1].name = 'Category'
})
In the extreme case, you can combine immer with lodash to create mutations in arbitrary places:
import set from 'lodash/set'
const newState = immer(oldState, draft => {
set(draft, [0, 'subcategories', 5, 'subcategories', 3], { id: 5, name:'Cat' })
})
"You might not need lodash" website has the recursive implementation for lodash/set. But, seriously, just use lodash.
PLUSES:
If you are using redux-toolkit, immer already is auto-applied on reducers, and is exposed as createNextState (check docs with care).
Deeply nested state can be an interesting use case for normalizr (long talk).
this is how the recursive function would look like.
the arguments:
id: id to look for
cats: the categories array to loop
nestSubCategory: (boolean) if we want to add the subcategory object in the subCategories array or not
subCategory: the category object we want to insert
const addCategories = (id, cats, nestSubCategory, subCategory)=> {
const cat = cats.find(item=> item.id === id)
const arrSubs = cats.filter(item => item.subCategories?.length)
.map(item => item.subCategories)
if(cat){
if(nestSubCategory){
cat.subCategories.push(subCategory)
return
}else{
cats.push(subCategory)
return
}
}
else{
return addCategories(id, arrSubs[0], nestSubCategory, subCategory)
}
}
addCategories("blabla1", categories, true, { id: "blabla2", name: "blablacategory1", subCategories: [] })
//console.log(categories)
console.log(
JSON.stringify(categories)
)
remember to update the object in the state replacing the entire categories array once the function is executed.
be careful with recursion 🖖🏽
you can do in a similar way to remove items, i leave to you the pleasure to experiment
I need help, I have problem to add a new items to my Object useState,
I want to add items array of object a new item, can some one help me?
my useState :
const [kolom, setKolom] = useState({
["Main-Todo"]: {
columnName: "Todo",
items: [
{
id: v4(),
content: "Makan ayam di MCD 🍗",
},
{
id: v4(),
content: "Main bola sama presiden Putin ⚽",
},
],
},
[v4()]: {
columnName: "In-Progress",
items: [],
},
});
here's my code try to add items array of object in useState :
setKolom({
...kolom,
["Main-Todo"]: {
items: addTask,
},
});
}}
Thanks a lot, i new at react js
Use the functional update form to update the existing value and merge your change in with the current Main-Todo object
setKolom((prev) => ({
...prev, // current kolom
"Main-Todo": {
...prev["Main-Todo"], // current kolom["Main-Todo"]
items: addTask, // new items
},
}));
If by "add items array" you mean you want to append to the existing array as opposed to replacing it, you need only change one line...
items: prev["Main-Todo"].items.concat(addTask)
Using Array.prototype.concat() works if addTask is an array or a single object.
If you can use immerjs it can be written this way:
setKolom(produce(draft => {
draft["Main-Todo"].items.push(addedTask),
}))
I have a State of objects in an Array (in my Redux Reducer).
const initialState = {
items: [
{ id: 1, dish: "General Chicken", price: 12.1, quantity: 0 },
{ id: 2, dish: "Chicken & Broccoli", price: 10.76, quantity: 0 },
{ id: 3, dish: "Mandaran Combination", price: 15.25, quantity: 0 },
{ id: 4, dish: "Szechuan Chicken", price: 9.5, quantity: 0 }
],
addedItems: [],
total: 0
};
I have an action to add 1 to the quantity of an object, such as
{id:1, dish: Generals Chicken, price: 10.76, quantity:0}
when a button in clicked in Cart.jsx. Here's the first Reducer I tried using the spread operator:
case "ADD_QUANTITY":
let existing_item = state.addedItems.find(
item => action.payload === item.id
);
return {
...state,
addedItems: [
...state.addedItems,
{ ...existing_item, quantity: existing_item.quantity + 1 }
]
};
This didn't work, instead of adding 1 to the quantity, it added another object with the quantity set to 2. . .So, I tried using Map like this
case "ADD_QUANTITY":
return {
...state,
addedItems: state.addedItems.map(item =>
item.id === action.payload
? { ...item, quantity: item.quantity + 1 }
: item
)
};
And this worked correctly. My question is, why didn't the spread operator work? As far as I can tell, it should do the same thing as the Map?
The two pieces of code are quite different.
The first one creates a new array out of state.addedItems and the new object { ...existing_item, quantity: existing_item.quantity + 1 }, and then assigns that array to the addedItems property of the state.
The second piece of code iterates addedItems and if it finds an element with the same id as the payload, it creates a new object { ...item, quantity: item.quantity + 1 } and returns that one, instead of the original item from the array.
Thus, even though both approaches create a new array, the first one has an extra object compared to the original, while the second has an object with a modified property.
The spread syntax, when used in an array literal context, does not reproduce the keys (the indexes), but just the values. As opposed to the spread syntax in an object literal context, which produces key/value pairs. The latter allows previous entries to be overruled by a new entry, having the same key, but the first does not have this behaviour: it always spreads all values without regards for indexes.
When replacing an element in an array, while copying it, you need to:
know the index at which the substitution should be performed, and
ensure that the copy is an instance of Array, and not just a plain object
You can use findIndex and Object.assign([], ) for addressing those needs:
case "ADD_QUANTITY":
let index = state.addedItems.findIndex(
item => action.payload === item.id
);
existing_item = state.addedItems[index];
return {
...state,
addedItems: Object.assign([], state.addedItems, {
[index]: { ...existing_item, quantity: existing_item.quantity + 1 }
})
}
Its because in your spread example, its has no way to tell which object it should overwrite. So it doesn't operate quite the same as some other examples you might see. Consider the following:
If you had an object like this:
let test = { a: 'first', b: 'second' }
Then doing spread like this would work:
let newTest = {...test, b: 'third' }
The reason is you are specifying that b should be overwritten.
When you try to create the same effect with an array of objects, you can't specify the key. So what you're actually doing is just appending the new object to the end of the array.
In the map example, you are checking the object contents and returning a different object based on if it matches your condition, so you know which object to overwrite.
I'm working on a react project where I need to filter an array of objects without mutating the original array
const array = [{
name: 'bar',
children: [{
name: 'foo',
children: [{
name: 'baz123',
}, {
name: 'baz',
}]
}]
}, {
name: 'shallowKey'
}, {
name: 'abc'
}];
For example, I want to only filter the concerned object and its children.
This is the jsfiddle
function filterData(data) {
var r = data.filter(function(o) {
if (o.children) o.children = filterData(o.children);
return o.name.length === 3;
})
return r;
}
I tried that function from a stackoverflow question, but is there a way to use that same functionality without mutating the data. Thanks
If you don't have any prototypes or functions involved within the objects a simple way to copy is to stringify original and parse it back to object
var r= JSON.parse(JSON.stringify(data)).filter(...
Array .filter() already creates a new array, you just need to fix the part where the o.children is mutated. To do that you could use .map() and simply copy all fields using Object.assign() or object spread and just assign children as the result passed through the same filter function:
function filterData(data) {
return data
.filter(obj => obj.name.length === 3) // filter array first
.map(obj => ({ // then re-map to new objects
...obj, // copy shallow fields
children: obj.children && filterData(obj.children) // filter children
}));
}
You can create a copy of your original array using a spread operator or Object.assign() function.
const arrayCopy= [...array] //spread operator
const arrayCopy = Object.assign({}, array);
Otherwise as Aaron suggested, using filter(), map(), reduce() function always returns a new array without mutating your original array.
I am trying to assign clone/merge object, that has array where id matches or add to an end:
newState = Object.assign({}, state, {
data: {
newest: {
result: action.payload.result,
list: action.payload.items,
request: action.payload.items
},
itemList: [
...state.data.itemList,
{
id: action.payload.id,
list: action.payload.items,
request: action.payload.items
}
]
}
});
In this case ...state.data.itemList is an array with objects where I want to find an existing object with ID and merge list + request nested objects. However, if there is no object with that ID I want to add it to the list.
Current approach always adds it to the end, which of course is not what I want.
Thanks.
Don't feel pressured into making the new state in one operation. Here would be my implementation. I would check my array for the current id using Array#some. Here are some fun ES6 tricks too.
const newState = {...state}
const {result, items, id, items} = action.payload
const data = {newest: {result, list: items, request: items}}
const hasId = newState.data.itemList.some(item => item.id === id)
if (!hasId) {
newState.data.itemList.push({id, list: items, request: items})
}
return {...newState, data}