SetState of an array of Objects in React - reactjs

Ok, so I'm so frustrated finding the right solution so I'm posting the problem here. Giving an answer would help me a lot, coz I'm stuck!
the state tree looks like this
this.state = {
itemList : [{
_id : 1234,
description : 'This the description',
amount : 100
}, {
_id : 1234,
description : 'This the description',
amount : 100
}],
}
The problems are :
can not update any specific key in the Object of the array according
to the _id
The previous state should remain intact

answered March 25 2018
This is how you would use setState and prevstate to update a certain attribute of an object in your data structure.
this.setState(prevState => ({
itemList: prevState.itemList.map(
obj => (obj._id === 1234 ? Object.assign(obj, { description: "New Description" }) : obj)
)
}));
answered Dec 12 2019 (REACT HOOKS)
import React, { useState } from 'react';
const App = () => {
const [data, setData] = useState([
{
username: '141451',
password: 'password',
favoriteFood: 'pizza',
},
{
username: '15151',
password: '91jf7jn38f8jn3',
favoriteFood: 'beans'
}
]);
return (
<div>
{data.map(user => {
return (
<div onClick={() => {
setData([...data].map(object => {
if(object.username === user.username) {
return {
...object,
favoriteFood: 'Potatos',
someNewRandomAttribute: 'X'
}
}
else return object;
}))
}}>
{JSON.stringify(user) + '\n'}
</div>
)
})}
</div>
)
}

to update state constructed like this you will have to find index of element you want to update, copy the array and change found index.
it's easier and more readable if you keep list of records as object, with id as a key and record as a value.

The only way to do this will be to copy itemList, modify it, and set the state to it.
update() {
let itemList = this.state.itemList.slice();
//update it
this.setState({ itemList });
}

Best way to update data into an array of objects
onChange={ (e) => this.setState({formData: { ...this.state.formData, 'plan_id': e.target.value}})}

Related

Update Object Key Value with onClick [duplicate]

Is it at all possible to update object's properties with setState?
Something like:
this.state = {
jasper: { name: 'jasper', age: 28 },
}
I have tried:
this.setState({jasper.name: 'someOtherName'});
and this:
this.setState({jasper: {name: 'someothername'}})
The first results in a syntax error and the second just does nothing. Any ideas?
There are multiple ways of doing this, since state update is a async operation, so to update the state object, we need to use updater function with setState.
1- Simplest one:
First create a copy of jasper then do the changes in that:
this.setState(prevState => {
let jasper = Object.assign({}, prevState.jasper); // creating copy of state variable jasper
jasper.name = 'someothername'; // update the name property, assign a new value
return { jasper }; // return new object jasper object
})
Instead of using Object.assign we can also write it like this:
let jasper = { ...prevState.jasper };
2- Using spread syntax:
this.setState(prevState => ({
jasper: { // object that we want to update
...prevState.jasper, // keep all other key-value pairs
name: 'something' // update the value of specific key
}
}))
Note: Object.assign and Spread Operator creates only shallow copy, so if you have defined nested object or array of objects, you need a different approach.
Updating nested state object:
Assume you have defined state as:
this.state = {
food: {
sandwich: {
capsicum: true,
crackers: true,
mayonnaise: true
},
pizza: {
jalapeno: true,
extraCheese: false
}
}
}
To update extraCheese of pizza object:
this.setState(prevState => ({
food: {
...prevState.food, // copy all other key-value pairs of food object
pizza: { // specific object of food object
...prevState.food.pizza, // copy all pizza key-value pairs
extraCheese: true // update value of specific key
}
}
}))
Updating array of objects:
Lets assume you have a todo app, and you are managing the data in this form:
this.state = {
todoItems: [
{
name: 'Learn React Basics',
status: 'pending'
}, {
name: 'Check Codebase',
status: 'pending'
}
]
}
To update the status of any todo object, run a map on the array and check for some unique value of each object, in case of condition=true, return the new object with updated value, else same object.
let key = 2;
this.setState(prevState => ({
todoItems: prevState.todoItems.map(
el => el.key === key? { ...el, status: 'done' }: el
)
}))
Suggestion: If object doesn't have a unique value, then use array index.
This is the fastest and the most readable way:
this.setState({...this.state.jasper, name: 'someothername'});
Even if this.state.jasper already contains a name property, the new name name: 'someothername' with be used.
Use spread operator and some ES6 here
this.setState({
jasper: {
...this.state.jasper,
name: 'something'
}
})
I know there are a lot of answers here, but I'm surprised none of them create a copy of the new object outside of setState, and then simply setState({newObject}). Clean, concise and reliable. So in this case:
const jasper = { ...this.state.jasper, name: 'someothername' }
this.setState(() => ({ jasper }))
Or for a dynamic property (very useful for forms)
const jasper = { ...this.state.jasper, [VarRepresentingPropertyName]: 'new value' }
this.setState(() => ({ jasper }))
Using hook we can do following way
const [student, setStudent] = React.useState({name: 'jasper', age: 28});
setStudent((prevState) => ({
...prevState,
name: 'newName',
}));
I used this solution.
If you have a nested state like this:
this.state = {
formInputs:{
friendName:{
value:'',
isValid:false,
errorMsg:''
},
friendEmail:{
value:'',
isValid:false,
errorMsg:''
}
}
}
you can declare the handleChange function that copy current status and re-assigns it with changed values
handleChange(el) {
let inputName = el.target.name;
let inputValue = el.target.value;
let statusCopy = Object.assign({}, this.state);
statusCopy.formInputs[inputName].value = inputValue;
this.setState(statusCopy);
}
here the html with the event listener. Make sure to use the same name used into state object (in this case 'friendName')
<input type="text" onChange={this.handleChange} " name="friendName" />
try this,it should work fine
this.setState(Object.assign(this.state.jasper,{name:'someOtherName'}));
Create a state object
this.state = {
objName: {
propertyOne: "",
propertyTwo: ""
}
};
Update state using setState
this.setState(prevState => ({
objName: {
...prevState.objName,
propertyOne: "Updated Value",
propertyTwo: "Updated value"
}
}));
this is another solution using immer immutabe utility, very suited for deeply nested objects with ease, and you should not care about mutation
this.setState(
produce(draft => {
draft.jasper.name = 'someothername'
})
)
Using hooks in Functional Component:
const [state, setState] = useState({jasper: { name: 'jasper', age: 28 }})
const nameChangeHandler = () => {
setState(prevState => ({
...prevState,
prevState.jasper.name = "Anurag",
prevState.jasper.age = 28
})
)
}
In these cases It is recommended to use callback-based approach to update the state , because using this approach it is ensured that previously states are fully updated and we're updating based on previously updated state.
The first case is indeed a syntax error.
Since I can't see the rest of your component, it's hard to see why you're nesting objects in your state here. It's not a good idea to nest objects in component state. Try setting your initial state to be:
this.state = {
name: 'jasper',
age: 28
}
That way, if you want to update the name, you can just call:
this.setState({
name: 'Sean'
});
Will that achieve what you're aiming for?
For larger, more complex data stores, I would use something like Redux. But that's much more advanced.
The general rule with component state is to use it only to manage UI state of the component (e.g. active, timers, etc.)
Check out these references:
https://facebook.github.io/react/docs/react-component.html#state
https://facebook.github.io/react/docs/state-and-lifecycle.html
In case of updating an object where keys are string
e.g. let say your state object is
serviceDays: {
Sunday: true,
Monday: true,
Tuesday: false,
Wednesday: true,
Thurday: false,
Friday: true,
Saturday: true
}
so you can update in following way
const onDayClick = day => {
const { serviceDays } = this.state
this.setState(prevState => ({
serviceDays: {
...prevState.serviceDays,
[day]: serviceDays[day] ? false : true
}
}))
}
Another option: define your variable out of the Jasper object and then just call a variable.
Spread operator: ES6
this.state = { jasper: { name: 'jasper', age: 28 } }
let foo = "something that needs to be saved into state"
this.setState(prevState => ({
jasper: {
...jasper.entity,
foo
}
})
You can try with this:
this.setState(prevState => {
prevState = JSON.parse(JSON.stringify(this.state.jasper));
prevState.name = 'someOtherName';
return {jasper: prevState}
})
or for other property:
this.setState(prevState => {
prevState = JSON.parse(JSON.stringify(this.state.jasper));
prevState.age = 'someOtherAge';
return {jasper: prevState}
})
Or you can use handleChage function:
handleChage(event) {
const {name, value} = event.target;
this.setState(prevState => {
prevState = JSON.parse(JSON.stringify(this.state.jasper));
prevState[name] = value;
return {jasper: prevState}
})
}
and HTML code:
<input
type={"text"}
name={"name"}
value={this.state.jasper.name}
onChange={this.handleChange}
/>
<br/>
<input
type={"text"}
name={"age"}
value={this.state.jasper.age}
onChange={this.handleChange}
/>
You can try with this:
(Note: name of input tag === field of object)
<input name="myField" type="text"
value={this.state.myObject.myField}
onChange={this.handleChangeInpForm}>
</input>
-----------------------------------------------------------
handleChangeInpForm = (e) => {
let newObject = this.state.myObject;
newObject[e.target.name] = e.target.value;
this.setState({
myObject: newObject
})
}
Simple and dynamic way.
This will do the job, but you need to set all the ids to the parent so the parent will point to the name of the object, being id = "jasper" and name the name of the input element = property inside of the object jasper.
handleChangeObj = ({target: { id , name , value}}) => this.setState({ [id]: { ...this.state[id] , [name]: value } });
Without using Async and Await Use this...
funCall(){
this.setState({...this.state.jasper, name: 'someothername'});
}
If you using with Async And Await use this...
async funCall(){
await this.setState({...this.state.jasper, name: 'someothername'});
}
Also, following Alberto Piras solution, if you don't want to copy all the "state" object:
handleChange(el) {
let inputName = el.target.name;
let inputValue = el.target.value;
let jasperCopy = Object.assign({}, this.state.jasper);
jasperCopy[inputName].name = inputValue;
this.setState({jasper: jasperCopy});
}
Try with this:
const { jasper } = this.state; //Gets the object from state
jasper.name = 'A new name'; //do whatever you want with the object
this.setState({jasper}); //Replace the object in state
By using the input html input name attribute we can have a more dynamic approach in order to update an object properties.
DOM
html input name attribute
<input type="text" name="fname" handleChange={(e: any) => { updatePerson(e) }}/>
<input type="text" name="lname" handleChange={(e: any) => { updatePerson(e) }}/>
React / TSX
object.assign
const [person, setPerson] = useState<IPerson>({});
function updatePerson(e: React.ChangeEvent<HTMLInputElement>): void {
const { name, value } = e.currentTarget;
setPerson(prevState => {
const newState = Object.assign(person, { [name]: value })
return { ...prevState, ...newState };
});
}
Sample FC:
const [formData, setformData] = useState({
project_admin_permissions: {
task_forms: false,
auto_assign_rules: false,
project_notes: true,
alerts: false,
update_criteria: true,
project_flow: false,
reports: false,
}
})
const handleChangeCheckBox = (e) => {
setformData({
...formData, project_admin_permissions: { ...formData.project_admin_permissions, [e.target.name]: e.target.checked }
})
}
This setup worked for me:
let newState = this.state.jasper;
newState.name = 'someOtherName';
this.setState({newState: newState});
console.log(this.state.jasper.name); //someOtherName
Your second approach doesn't work because {name: 'someothername'} equals {name: 'someothername', age: undefined}, so theundefined would overwrite original age value.
When it comes to change state in nested objects, a good approach would be Immutable.js
this.state = {
jasper: Record({name: 'jasper', age: 28})
}
const {jasper} = this.state
this.setState({jasper: jasper.set(name, 'someothername')})

run function once a value in an object has been updated

I have a list of objects that have multiple fields. I want to make an api call once the user has entered the location of the plant.
I thought of using useEffect, but that won't work since in order to make the api call, I need two values - id and index, and (as far as I know) you can't pass parameters in useEffect. I did it normally (which you can see in the code below), instead of using useEffect, but that doesn't work because the plant location is empty (as there is a bit of a delay before the plant location is updated).
So, how can I make the call once the plant location has been updated?
//the plants object
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
isOffshore: false,
coords: {},
country: '',
}
])
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
handler(id, index);
}
// this makes the api call.
//I need to pass id and index to this function
//no matter what, hence why I can't use useEffect
const getCoords = (id, index) => {
axios.get('http://localhost:3001/endpoints/coords', {
params: {
location: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 4000), []);
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} disabled={item.isOffshore} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
you can pass props and state to useEffect like this:
useEffect(() => {
getCoords(id, index);
return () => getCoords(id, index);
}, [id, index])
Please use useEffect hook with dependency array.
Like below:
useEffect(() => {
// You can call API here because When plant state updated this will be called.
}, [plants]);

Functional component problems React

I transformed a class component into a functional component but it looks like it does not work in a way it suppose to work and I can not find what is wrong. When I create a new object there is no name for the object and when I try to mark the object as a complete it removes all created objects at ones. I created a codesandbox here. Unfortunately, I am not too much familiar with functional component. Any help would be appreciated.
Here is my codesandbox sample:
https://codesandbox.io/s/crazy-sid-09myu?file=/src/App.js
Your Todos:
const [todos, setTodos] = useState([
{ id: uuid(), name: "Task 1", complete: true },
{ id: uuid(), name: "Task 2", complete: false }
]);
onAddHandler:
const addTodo = () =>
setTodos([...todos, { id: uuid(), name: "New Task", complete: false }]);
onSetCompleteHandler:
const setCompleteHandler = id =>
setTodos(
todos.map(todo => {
if (todo.id === id) {
return {
...todo,
complete: todo.complete ? 0 : 1
};
}
return todo;
})
);
I have created your new todos. Check out this link
Todos App
I have updated your code, please check the URL https://codesandbox.io/s/determined-morning-n8lgx?file=/src/App.js
const onComp = id => {
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
let t = { ...todos[i] };
t.complete = !t.complete;
todos[i] = t;
}
}
setTodos([...todos]); // Here todos reference need to be changed
};
And also
const onSubmit = event => {
event.preventDefault();
setTodos([
...todos,
{
id: generateNewId(),
name: newTodoName,
complete: false
}
]);
setNewTodoName("");
};
While using hooks we need to be careful about state variable updates. While manipulating arrays and objects use the spread operator to create new references which invokes child components and re-render current component.

React setState changing different object in state

This is my initial state of component
this.state = {
date: "",
students: "",
attendance: {}
}
On ComponentDidMount I am getting date and students array from a reducer.
componentDidMount() {
const { date, students } = this.props;
const todayDate = this.getFormattedDate(date);
this.setState(prevState => ({
date: todayDate,
students,
attendance: {
...prevState.attendance,
[todayDate]: students
}
}), () => console.log('CDM', this.state));
}
I have a checkbox which when touched should update attendance[todayDate] and update property checked of a particular student.
In render I am mapping over attendance[date] and returning JSX
<CheckBox
checked={student.checked}
onPress={() => this.takeAttendance(student.id)}
containerStyle={styles.checkboxStyle}
/>
The checkHandler
takeAttendance(id) {
this.setState(prevState => ({
attendance: {
...prevState.attendance,
[this.state.date]: prevState.attendance[this.state.date].map(
obj => (obj.id === id ? Object.assign(obj, { checked: !obj.checked }) : obj)
)
}
}), () => console.log('TA', this.state));
}
So it's working correctly. The property checked of a student in attendance[date] is getting changed but its also changing the checked value in students piece of state. I do not understand why it is also changing the students piece of state and not just attendance[date].
Added video for reference. Streamable
Umm looks all correct, may be try addressing the this.state.date to prevState.date?
attendance: {
...prevState.attendance,
[prevState.date]: prevState.attendance[prevState.date].map(
obj => (obj.id === id ? Object.assign(obj, { checked: !obj.checked }) : obj)
)
}
Edit:
Could be issue with this.setState(prevState => ({ date: todayDate, students,. You are setting the students same as [todayDate]: students.
Add property checked to each individual student and stored that in attendance[date].
try to check deep clone object
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone

React Duplicate Key Error

I'm getting the following error, I understand what its telling me but I can't figure out how to solve the issue.
flattenChildren(...): Encountered two children with the same key...
I have 2 lists on my page which contain emails. The initial state of my app contains the following data:
const initialState = {
emails: [
{
id: 1, from: 'test.1#test.co.uk', body: 'test1 body', title: 'test 1 title',
},
{
id: 2, from: 'test.2#test.co.uk', body: 'test2 body', title: 'test 2 title',
},
],
draggedEmails: [],
};
The UI of my app lets you drag and drop items from the first list (emails) to the second list (draggedEmails).
In my Redux reducer I have the following code to move emails between the lists.
let newState = {};
//Check if the email exists in my 'emails' array
const doesExistInEmails = state.emails.find(x => x.id === action.id) !== null;
//If it exists then move it to the 'draggedEmails' list
if (doesExistInEmails) {
const filteredEmails = state.emails.filter(e => e.id !== action.emailItem.id);
newState = Object.assign(
{},
state,
{ draggedEmails: [...state.draggedEmails, action.emailItem], emails: filteredEmails }
);
} else {
const filteredEmails = state.emails.filter(e => e.id !== action.emailItem.id);
newState = Object.assign(
{},
state,
{ draggedEmails: [...state.emails, action.emailItem], emails: filteredEmails });
}
return newState;
The problem occurs when I move the items BACK to the emails list, once they have been moved to the 'draggedEmails' list.
The following code is what is used to create the elements and the keys.
createEmailItem(em) {
return React.createElement(
EmailItem, { email: em, key: `${em.id}` });
}
Any help is appreciated,
Thanks.
EDIT: Console.Logged state after moving one item from the 'emails' list to the 'draggedEmails' list. Everything looks as it should.
Object {emails: Array[1], draggedEmails: Array[1]}
EDIT2: Adding render method.
render() {
return (
<div className="email-container" onDrop={this.onDrop} onDragOver={this.allowDrop}>
{this.props.emails.map(this.createEmailItem)}
</div>
);
}
I found the problem. There were 4.
The first is that the following was returning 'undefined' rather than 'null'
const doesExistInEmails = state.emails.find(x => x.id === action.id) !== null;
The second is that my action doesn't have an id, my action has an emailItem which has an id
const doesExistInEmails = state.emails.find(x => x.id === action.emailItem.id) !== undefined;
The third is that I was filtering my emails rather than dragged emails in the following line.
const filteredEmails = state.filter(e => e.id !== action.emailItem.id);
And finally I was assigning the wrong values back when setting the state.
{ draggedEmails: [...state.emails, action.emailItem], emails: filteredEmails });
Should be...
{ emails: [...state.emails, action.emailItem], draggedEmails: filteredEmails });
So overall, I had lots wrong...
Thanks to the guys who commented.

Resources