React - updating an object array in the state with setState - javascript

I'm working on a table planner app where guests can be assigned to dinner tables.
I have created an object array in the state called tabledata, which will contain objects like so:
this.state = {
tabledata: [
{
name: "Top Table",
guests: ["guest1", "guest2", "guest3"]
},
{
name: "Table One",
guests: ["guest3", "guest4", "guest5"]
}
]
}
I am then creating a drag and drop interface where guests can move between tables. I have attempted to update the state like so:
updateTableList (tablename, guest) {
const selectedTableObj = this.state.tabledata.filter((tableObj) => tableObj.name === tablename);
const otherTableObjs = this.state.tabledata.filter((tableObj) => tableObj.name !== tablename);
selectedTableObj[0].guests.push(guest);
const updatedObjectArray = [...otherTableObjs, selectedTableObj];
this.setState({
tabledata: [...otherTableObjs, ...selectedTableObj]
});
}
This works but because I am removing selectedTableObj from the state and then adding it to the end of the array I'm getting some funky results on screen. The updated table always goes to the bottom of the page (as you'd expect).
How can I update the object without changing its position within the array?

Find the index of the table you want to update using Array.findIndex(). Create a new tabledata array. Use Array.slice() to get the items before and after the updated table, and spread them into the new tabledata array. Create a new table object using object spread, add the updated guests array, and add the table object between the previous items:
Code (not tested):
updateTableList(tablename, guest) {
this.setState((prevState) => {
const tableData = prevState.tabledata;
const selectedTableIndex = tableData.findIndex((tableObj) => tableObj.name === tablename);
const updatedTable = tableData[selectedTableIndex];
return {
tabledata: [
...prevState.tabledata.slice(0, selectedTableIndex),
{
...updatedTable,
guests: [...updatedTable.guests, guest]
},
...prevState.tabledata.slice(selectedTableIndex + 1)
]
};
});
}

selectedTableObj[0].guests.push(guest) directly mutates the state which is not encouraged in React.
Try this:
this.setState((prevState) => {
const newData = [...prevState.tabledata];
// if you pass in `index` as argument instead of `tablename` then this will not be needed
const index = prevState.tabledata.findIndex(table => tableObj.name === tablename);
newData[index] = {
...newData[index],
guests: newData[index].guests.concat([guest]),
};
return { tabledata: newData };
});
You also did not remove the guest from its previous table so you need to modify for that.

You can do it with a Array.reduce
let newState = this.state
// let newState = {...this.state} // in case you want everything immutable
newState.tableData = newState.tableData.reduce((acc, table) =>
if(table.name === tableName) {
return acc.concat({...table, guests: table.guests.concat(newGuest)})
} else {
return acc.concat(table)
}
)

Related

Redux Toolkit state object is not changing "Uncaught TypeError: Cannot assign to read only property 'property-name' of object '#<Object>'"

I want to change the data of a state object but Redux Toolkit is not changing and gives an error like Uncaught TypeError: Cannot assign to read only property 'status' of object '#<Object>'.
These lines from component:
const [width, setWidth] = useState(8)
const [height, setHeight] = useState(9)
const [table, setTable] = useState({
rows:
[...Array(height)].map(()=>
(
{
cells:
[...Array(width)].map(()=>
(
{status: true}
)
)
}
)
)
})
useEffect(()=>{
dispatch(changePlayableFields(table)) // <- it's not changing the state
},[table])
function changeCell(i:number,k:number){
const localTable = {...table}
localTable.rows[i].cells[k].status = !localTable.rows[i].cells[k].status // <-Uncaught TypeError: Cannot assign to read only property 'status' of object '#<Object>'
setTable(localTable)
}
changeCell function is working very well and I see the truth results on the page. But when adding useEffect codes to move new datas to keep them in memory with redux, then I get the errors.
And these are from Redux Slice:
import { createSlice } from "#reduxjs/toolkit"
const levelSlice = createSlice({
name: "level",
initialState: {
gridSizeAndPlayableFields: {
width: 8,
height: 9,
playableFields: {
rows:
[...Array(9)].map(()=>
(
{
cells:
[...Array(8)].map(()=>
(
{status: true}
)
)
}
)
)
}
},
},
reducers: {
changePlayableFields: (state, action) => {
state.gridSizeAndPlayableFields.playableFields = action.payload // <- it's not changing the data
},
}
})
export const {changeGridSize, changePlayableFields} = levelSlice.actions
export default levelSlice.reducer
It's little about my previous question, maybe you'd like to check it. Here is my previous question link: Redux Slice is returning an error after adding a function into a property of initial states
I hope anyone can help. Thanks...
1. Reducer does not update issue:
It seems you are sending rows as payload of the dispatch. So you should update gridSizeAndPlayableFields.playableFields.rows in the reducer:
changePlayableFields: (state, action) => {
// console.log(action.payload);
state.gridSizeAndPlayableFields.playableFields.rows = action.payload.rows;
// console.log(current(state.gridSizeAndPlayableFields.playableFields));
},
2. object update issue in changeCell method:
In a normal function you cannot mutate the object like in changeCell function. You can only do this in the redux toolkit slice thanks to Immer.
You can map the rows and cells arrays to update the corresponding indeces' status values. You can write the changeCell method in the following way:
function changeCell(i, k) {
if (!table) return;
const localTable = {};
const localTableRows = [...table.rows];
// map rows array
const updatedRows = localTableRows.map((item, index) => {
// if index equals i, map its cells as well
//... and find kth cell and change its status
if (index === i) {
return {
...item,
cells: item.cells.map((c, idx) => {
if (idx === k) {
return {
...c,
status: !c.status,
};
}
// if idx not equals to k return old cell item
return c;
}),
};
}
// if index is not equal i return old row item
return item;
});
localTable.rows = updatedRows;
setTable(localTable);
}
I suppose i and k index values for the zero based array.
You can use Object.reduce method to calculate updated table object in your changeCell function; but personally I try to avoid from this because in general Object.reduce is less performant.

How to update a javascript object keys value using the index of an object [duplicate]

If you have an array as part of your state, and that array contains objects, whats an easy way to update the state with a change to one of those objects?
Example, modified from the tutorial on react:
var CommentBox = React.createClass({
getInitialState: function() {
return {data: [
{ id: 1, author: "john", text: "foo" },
{ id: 2, author: "bob", text: "bar" }
]};
},
handleCommentEdit: function(id, text) {
var existingComment = this.state.data.filter({ function(c) { c.id == id; }).first();
var updatedComments = ??; // not sure how to do this
this.setState({data: updatedComments});
}
}
I quite like doing this with Object.assign rather than the immutability helpers.
handleCommentEdit: function(id, text) {
this.setState({
data: this.state.data.map(el => (el.id === id ? Object.assign({}, el, { text }) : el))
});
}
I just think this is much more succinct than splice and doesn't require knowing an index or explicitly handling the not found case.
If you are feeling all ES2018, you can also do this with spread instead of Object.assign
this.setState({
data: this.state.data.map(el => (el.id === id ? {...el, text} : el))
});
While updating state the key part is to treat it as if it is immutable. Any solution would work fine if you can guarantee it.
Here is my solution using immutability-helper:
jsFiddle:
var update = require('immutability-helper');
handleCommentEdit: function(id, text) {
var data = this.state.data;
var commentIndex = data.findIndex(function(c) {
return c.id == id;
});
var updatedComment = update(data[commentIndex], {text: {$set: text}});
var newData = update(data, {
$splice: [[commentIndex, 1, updatedComment]]
});
this.setState({data: newData});
},
Following questions about state arrays may also help:
Correct modification of state arrays in ReactJS
what is the preferred way to mutate a React state?
I'm trying to explain better how to do this AND what's going on.
First, find the index of the element you're replacing in the state array.
Second, update the element at that index
Third, call setState with the new collection
import update from 'immutability-helper';
// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] }
updateEmployee(employee) {
const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]}); // array.splice(start, deleteCount, item1)
this.setState({employees: updatedEmployees});
}
Edit: there's a much better way to do this w/o a 3rd party library
const index = this.state.employees.findIndex(emp => emp.id === employee.id);
employees = [...this.state.employees]; // important to create a copy, otherwise you'll modify state outside of setState call
employees[index] = employee;
this.setState({employees});
You can do this with multiple way, I am going to show you that I mostly used. When I am working with arrays in react usually I pass a custom attribute with current index value, in the example below I have passed data-index attribute, data- is html 5 convention.
Ex:
//handleChange method.
handleChange(e){
const {name, value} = e,
index = e.target.getAttribute('data-index'), //custom attribute value
updatedObj = Object.assign({}, this.state.arr[i],{[name]: value});
//update state value.
this.setState({
arr: [
...this.state.arr.slice(0, index),
updatedObj,
...this.state.arr.slice(index + 1)
]
})
}

Javascript: Recursion returning undefined

Why is my recursion returning undefined? I'm trying to "decode" nested children data from mongo which is returned as IDs like:
{
"_id": "613fd030f374cb62f8f91557",
"children": [
"613fd035f374cb62f8f9155b",
"613fd136f374cb62f8f91564",
"613fd1a5f374cb62f8f91571",
"613fd20bf374cb62f8f9157c"
],
...more data
}
My goal is to drill down and convert each child ID to the Object the ID represensents and convert their child IDs to objects then keep going until the child === [] (no children). I'm trying to have the initial parent (613fd030f374cb62f8f91557) have access to all multi-level nested children objects.
This is my code:
const get_documents = (documents) => {
// Loop through each document
documents.map((document) => {
if (document.parent === null) {
//convert children ids (_id) to array of objects
let dbData = [];
document.children.map((id) => {
let dbChildren = documents.find((x) => x._id === id);
dbData.push(dbChildren);
});
let formattedData = [];
dbData.map((child) => {
let formattedObject = {
id: child._id,
name: child.name,
depth: 0,
parent: child.parent,
closed: true,
children: child_recursion(child.children),
};
formattedData.push(formattedObject)
});
}
});
};
const child_recursion = (arr) => {
let dbData = [];
arr.map((id) => {
let dbChildren = documents.find((x) => x._id === id);
dbData.push(dbChildren);
});
let formattedData = [];
dbData.map((child) => {
let newChild = [];
if (child.children.length > 1) {
newChild = child_recursion(child.children);
}
let formattedObject = {
id: child._id,
name: child.name,
depth: 0,
parent: child.parent,
closed: true,
children: newChild,
};
formattedData.push(formattedObject);
if (newChild === []) {
return formattedData
}
});
};
What am I doing wrong in my recursion? Thank you for the help!
What is getting you here is mixing mutation with recursion which tends to make things a lot more messy.
What this line is telling me:
children: child_recursion(child.children),
is that you are always expecting child_recursion to return an array of formatted children.
However, in child_recursion you aren't always returning something. Sometimes you are mutating sometimes instead. Personally, I believe that it tends to be easier to wrap my head around not using mutation.
The process, therefore, should go something like this:
given an object
check if that object has children
if it does convert the children using this function
if it does not, stop recursion
return a new object, created from the input object with my children set to the output of the conversion.
In this way we can convert each child into an object with its children converted and so on.
Also it is somewhat strange that you are trying to convert all documents at once. Instead, as you gave in your question, you should focus on the object you are trying to convert and work downwards from there. If it is the case where objects can be both parents and children then you have a graph, not a tree and recursion would have to be handled differently than you are expecting.
We don't really need two functions to do this, just one and in the case where you already have the objects you are searching you can pass that along as well (if you don't just remove documents and get them from the db or some service instead). We can also use what is called an accumulator to set initial values before our recursion and track them as we recur.
const convert_children = (obj, documents) => {
const convert_children_acc = (obj, documents, parent, depth) => {
let partial_format = {
id: obj._id,
name: obj.name,
depth: depth,
parent: parent,
close: true
}
if (obj.children && obj.children.length === 0) {
return {
...partial_format,
children: []
}
} else {
return {
...partial_format,
children: obj.children.map(child => {
child = documents.find(x => child === x._id);
return convert_children_acc(child, documents, obj._id, depth+1)
})
}
}
}
return convert_children_acc(obj, documents, null, 0);
};
https://jsfiddle.net/5gaLw1y7/

Unwanted state changes in a class component with React

Long story short, I have a class component that constructs a poll. Before sending the data to the server I need to transform it a little so it fits the API request. I created a transformData method on my class component that transforms the data derived from the state. As a side effect it sets the data in separate this.state.data property so I can attach it with the API request. The problem is that the method mutates the other properties of the state.
transformData = () => {
const { title, sections } = this.state
const transformedSections = sections.map(section => {
delete section.isOpen
const transformedQuestions = section.questions.map(question => {
question.label = question.question
question.type = toUpper(question.type)
delete question.question
return question
})
section.questions = {
create: transformedQuestions,
}
return section
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
}
So I get this:
state: {
data: {...} //our transformed data
sections: {...} //transformed as well!!
}
instead of getting this:
state: {
data: {...} //our transformed data
sections: {...} //same before calling the method
I re-wrote the method with different approach — basically replaced all Array.map with Array.forEach and it worked as expected.
transformData = () => {
const { title, sections } = this.state
const transformedSections = []
sections.forEach(section => {
const transformedQuestions = []
section.questions.forEach(question => {
transformedQuestions.push({
label: question.question,
type: toUpper(question.type),
max: question.max,
min: question.min,
instruction: question.instruction,
isRequired: question.isRequired,
placeholder: question.placeholder,
})
})
transformedSections.push({
title: section.title,
questions: { create: transformedQuestions },
})
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
Can anyone explain what's going on here? How can I accidentally mutate a state property without explicitly calling this.setState on the aforementioned property? The thing is that the originally written method mutates the state even if I return the data object without calling this.setState whatsoever. Like so:
//This still mutates the state
return {
data: {
title,
sections: { create: transformedSections },
}
}
//without this!
//this.setState({
// data: {
// title,
// sections: { create: transformedSections },
// },
// })
Thanks!
javascript behave like this way,
its called variable referencing.
it works like pointer variable in C.
if your console those variable such as console.log(var1 == var2) it will show true cuz both references from same memory location
if you want to prevent mutate original variable then you have to create another brand new variable to mutate
like this way :
const { title, sections } = this.state
// create new variable following old one (spreading es6 way)
const tempSections = [...sections]
...
also
sections.forEach(section => {
const transformedQuestions = []
const tempQuestions = [...section.questions]
tempQuestions.forEach(question => {
...
always have to create a brand new variable of object/array/... to prevent auto mutation
for further info here
Issue here is of Shallow Copying :
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
// state is muate already
console.log("---- After map -----" , this.state);
You can run the below code snippet and check both console.log, and check for "isOpen": true
Hope this will clear all your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
state = {
title : "questions" ,
sections : [{
isOpen : true ,
questions : ["que1" , "que2" , "que3"]
}]
}
transfromData = () => {
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
console.log("---- After map -----" , this.state);
}
render() {
return (
<div>
<button onClick={this.transfromData}>transfromData</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
You should never update the state without using the setState method. It is asyncronous, and if you don't set it properly you never know what might happen - and that's what you're seeing in the first part of your answer. See the docs
By doing
section.questions = {
create: transformedQuestions,
}
you are improperly altering the state, so you'll see this.state.sections transformed as well, because each element inside this.state.sections has now an attribute questions that contains create with the value transformedQuestions

Making dynamic form inputs with React

how to update the nested fields in react forms .,
here i do
add a new item handleAddShareholder ,
delete an existing item handleRemoveShareholder,
change details of an item handleShareholderNameChange
then i will push a new array to shareholders.customize by this
handleAddcomp = idx => () => {
this.state.shareholders[idx].customize.push({ name: '' });
const shareholders = this.state.shareholders;
this.setState({ shareholders: shareholders });
};
but i can't able to update the field value of nested arrays in react.js
my code is here https://jsbin.com/fugemuy/edit?html,js,output
Use Object.assign for this purpose. It will clone current object.
handleAddcomp = idx => () => {
let shareholders = Object.assign({}, this.state.shareholders); //creating copy of object in state
shareholders[idx].customize.push({ name: '' })
this.setState({ shareholders });
};

Categories