Hello guys I'm trying to update the state of a nested object in react, I'm currently doing this:
handleChange({target: {id, value}}, type) {
this.setState(
state => ({
dwelling: (Object.assign(state.dwelling, {[id]: {[type]: value}}))
})
);
}
it comes from a formgroup:
<FormGroup controlId="spaces">
<ControlLabel>Dormitorios</ControlLabel>
<FormControl
componentClass="select"
value={dwelling.spaces.dorms}
placeholder="Seleccione"
onChange={e => this.handleChange(e, 'dorms')}
>
The problem is when I update the state of the sub object dwelling.spaces.dorms is created but when I try to place another property it replaces the old one instead of getting added:
Before Dwelling:
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
closets: "",
dorms: "",
pools: ""
},
subtype: "",
type: ""
}
After onChange for dwelling.spaces.dorms
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
dorms: "3",
},
subtype: "",
type: ""
}
After onChange for dwelling.spaces.closets
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
closets: "3",
},
subtype: "",
type: ""
}
This example uses ES6 spread operator to keep your old properties which is the equivalent of Object.assign.
So what was happening is you're not keeping your nested value.
this.setState({
dwelling: {
...this.state.dwelling,
[id]: {
...this.state.dwelling[id],
[type]: value
}
}
});
In your example you overwrote your value with a new object. Notice the bolded text below.
dwelling: (Object.assign(state.dwelling, {[id]: {[type]: value}}))
In the bolded text it specifically set a new object into state.dwelling without keeping the old values. So what we did is that we used the ES6 spread operator to help merge your old values with the new value
{
...this.state.dwelling[id],
[type]: value
}
I keep my form state in a complex object and to simplify my code I use this helper function in my "handleChange" function referenced by TextField, Select, etc..
export function updateObject(obj, keys, value) {
let key = keys.shift();
if (keys.length > 0) {
let tmp = updateObject(obj[key], keys, value);
return {...obj, [key]: tmp};
} else {
return {...obj, [key]: value};
}
}
React-redux/Material-UI example
let [formState, changeFormState] = useState({});
function handleChange(event) {
changeFormState(updateObject(formState, event.target.name.split('.'),
event.target.value));
}
<TextField className={classes.textfield} name='foo.bar' value=
formstate.foo.bar || ''} onChange={handleChange} />
Related
class App extends React.Component {
state = {
firstName: "",
lastName: "",
email: "",
country: "",
tel: "",
dateOfBirth: "",
favoriteColor: "",
weight: "",
gender: "",
file: "",
bio: "",
skills: {
html: false,
css: false,
javascript: false,
},
};
handleChange = (e) => {
const { name, value, type, checked } = e.target;
console.log(e)
if (type === "checkbox") {
this.setState({
skills: { ...this.state.skills, [name]: checked },
});
} else if (type === "file") {
console.log(type, "check here");
this.setState({ [name]: e.target.files[0] });
} else {
this.setState({ [name]: value });
}
console.log(this.state.skills)
};
what does this line do?
skills: { ...this.state.skills, [name]: checked }
The ...this.state.skills may be using the spread operator to copy the object but I do not know the meaning of [name]: checked, the purpose of it is to change the value of key in the skills obj to true but I don know how it can be.
This is a very good question, lots of stuff going on in the following line. So good catch to learn new stuff.
skills: { ...this.state.skills, [name]: checked }
named property
Normally we do obj = { "abc": 3 }, but what if the "abc" is stored in a variable name? That's why this [name] comes to play. Otherwise we have to do obj[name] = 3 which takes additional effort. Also notice the difference between it with obj["name"] = 3. These two versions are very different.
spread operator
If we do a = b for an object, it doesn't make a new memory for a.
To create a new a, we do a = { ...b }. It takes out every first level property out of b and borrow them into a new memory space for a. Essentially a spread operator implies a new variable.
However only a is a new variable, none of the things you borrowed from b is re-created unless they are primitive variables. So we can not call a spread operator a deep copy. Maybe we call it a shallow copy, i don't know the right name ;)
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 1 year ago.
I'm a bit new with ReactJS and I have a problem with my onChange function, it starts displaying after the second entry and it is one letter late every time. And I clearly don't understand why
Here is my code:
const [values, setValues] = useState({
latinName: "",
wingLength: "",
weight: "",
adiposity: "",
age: "",
howCaptured: "",
whenCaptured: "",
whereCaptured: "",
ringNumber: "",
takeover: "",
});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
console.log(values);
}
<input type="text" name="latinName" id="latinName" onChange={handleChange} value={values.latinName} />
Here my input
Here my result
useState hook's setState works async. As a result, during state update, you are still getting the previous value in the mean time.
You can use useEffect hook in your case.
useEffect(() => console.log(values), [values]);
Full code:
import { useEffect, useState } from "react";
export default function App() {
const [values, setValues] = useState({
latinName: "",
wingLength: "",
weight: "",
adiposity: "",
age: "",
howCaptured: "",
whenCaptured: "",
whereCaptured: "",
ringNumber: "",
takeover: ""
});
useEffect(() => console.log(values), [values]);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};
return (
<input
type="text"
name="latinName"
id="latinName"
onChange={handleChange}
value={values.latinName}
/>
);
}
CodeSandBox Demo
I have the following state:
const [state, setState] = React.useState({
title: "",
exchangeTypes: [],
errors: {
title: "",
exchangeTypes: "",
}
})
I am using a form validation in order to populate the state.errors object if a condition is not respected.
function formValidation(e){
const { name, value } = e.target;
let errors = state.errors;
switch (true) {
case (name==='title' && value.length < 4):
setState(prevState => ({
errors: { // object that we want to update
...prevState.errors, // keep all other key-value pairs
[name]: 'Title cannot be empty' // update the value of specific key
}
}))
break;
default:
break;
}
}
When I do so, the object DOES update BUT it deletes the value that I have not updated.
Before I call formValidation
My console.log(state) is:
{
"title": "",
"exchangeTypes: [],
"errors": {
title: "",
exchangeTypes: "",
}
}
After I call formValidation
My console.log(state) is:
{
"errors": {
title: "Title cannot be empty",
exchangeTypes: ""
}
}
SO my other state values have disappeared. There is only the errors object.
I followed this guide: How to update nested state properties in React
What I want:
{
"title": "",
"exchangeTypes: [],
"errors": {
title: "Title cannot be empty",
exchangeTypes: "",
}
}
What I get:
{
"errors": {
title: "Title cannot be empty",
exchangeTypes: "",
}
}
unlike the setState in class component, setState via useState doesn't automatically merge when you use an object as the value. You have to do it manually
setState((prevState) => ({
...prevState, // <- here
errors: {
// object that we want to update
...prevState.errors, // keep all other key-value pairs
[name]: 'Title cannot be empty', // update the value of specific key
},
}));
Though you can certainly use the useState hook the way you're doing, the more common convention is to track the individual parts of your component's state separately. This is because useState replaces state rather than merging it, as you've discovered.
From the React docs:
You don’t have to use many state variables. State variables can hold objects and arrays just fine, so you can still group related data together. However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
So in practice, your code might look like the following:
const MyComponent = () => {
const [title, setTitle] = useState('');
const [exchangeTypes, setExchangeTypes] = useState([]);
const [errors, setErrors] = useState({
title: "",
exchangeTypes: "",
});
function formValidation(e) {
const { name, value } = e.target;
switch (true) {
case (name === 'title' && value.length < 4):
setErrors({
...errors,
[name]: 'Title cannot be empty'
});
break;
default:
break;
}
}
return (
...
);
};
I need to be able to combine this into one function. There are 2 separate arrays...
{this.state.telephoneType.map((telephoneType, ttidx) => ())}
and...
{this.state.telephone.map((telephone, tidx) => ())}
Basically, this is because I have a button which concatenates the 2 functions and it has to be outside the row class (MDBRow) so the UI doesn't break.
<MDBRow className="grey-text no-gutters my-2">
{this.state.telephoneType.map((telephoneType, ttidx) => (
<MDBCol md="4" className="mr-2">
<select
key={ttidx}
defaultValue={telephoneType.name}
onChange={this.handleTelephoneTypeChange(ttidx)}
className="browser-default custom-select">
<option value="Mobile">Mobile</option>
<option value="Landline">Landline</option>
<option value="Work">Work</option>
</select>
</MDBCol>
))}
{this.state.telephone.map((telephone, tidx) => (
<MDBCol md="7" className="d-flex align-items-center">
<input
value={telephone.name}
onChange={this.handleTelephoneChange(tidx)}
placeholder={`Telephone No. #${tidx + 1}`}
className="form-control"
/>
<MDBIcon icon="minus-circle"
className="mr-0 ml-2 red-text"
onClick={this.handleRemoveTelephone(tidx)} />
</MDBCol>
))}
</MDBRow>
<div className="btn-add" onClick={this.handleAddTelephone}>
<MDBIcon className="mr-1" icon="plus-square" />
Add Telephone
</div>
This is the handleAddTelephone function...
handleAddTelephone = () => {
this.setState({
telephone: this.state.telephone.concat([{ name: "" }]),
telephoneType: this.state.telephoneType.concat([{ name: "" }])
});
};
and the Constructor looks like this...
class InstallerAdd extends React.Component {
constructor() {
super();
this.state = {
role: "Installer",
name: "",
telephoneType: [{ name: "" }],
telephone: [{ name: "" }],
tidx: "",
emailType: [{ email: "" }],
email: [{ email: "" }],
eidx: "",
notes: ""
};
}
}
Can I nest one array inside the other? I'm not sure how to do this so any advice appreciated. Thanks.
Edit:
These are the 2 telephone functions which need to be 1 function...
I have updated with new nested array for each
handleTelephoneChange = tidx => evt => {
const newTelephone = this.state.telephone.type.map((telephone, tsidx) => {
if (tidx !== tsidx) return telephone;
return { ...telephone, name: evt.target.value };
});
this.setState({ telephone: newTelephone }, () => {
// get state on callback
console.log(this.state.telephone.number[tidx].name)
}
);
};
handleTelephoneTypeChange = ttidx => evt => {
const newTelephoneType = this.state.telephone.number.map((telephoneType, ttsidx) => {
if (ttidx !== ttsidx) return telephoneType;
return { ...telephoneType, name: evt.target.value };
});
this.setState({ telephoneType: newTelephoneType }, () => {
// get state on callback
console.log(this.state.telephone.type[ttidx].name)
}
);
};
My constructor now looks like this...
class InstallerAdd extends React.Component {
constructor() {
super();
this.state = {
role: "Installer",
name: "",
telephone: {
type: [{ name: "" }],
number: [{ name: "" }]
},
tidx: "",
emailType: [{ email: "" }],
email: [{ email: "" }],
eidx: "",
notes: ""
};
}
Although I still don't quite understand how your UI "breaks" (video won't load for me), I hope I can help.
Basically the short answer about trying to map two arrays singly is that you can't (well, shouldn't), but with some assumptions about the two array's length always being equal and the order is the same between the two, then you can map over the first array and use the passed index from the map function to access the second.
arrayA.map((itemFromA, index) => {
// do stuff with item from A
const itemFromB = arrayB[index];
// do stuff with item from B
});
I think the better solution is to keep only a single array in state to map over in the first place.
state = {
telephones: [], // { type: string, number: string }[]
}
...
state.telephones.map(({ type, number }, index) => {
// do stuff with telephone type
...
// do stuff with telephone number
});
If you also really want only a single change handler (I recommend they be separate), you can adopt a redux action/reducer type handler that accepts an object as a parameter that has all the data you need to update an array entry (index, update type, value). Here's a pretty clean example, but requires a bit of "action" setup when called:
changeHandler = (index, type) => e => {
const newTelephoneData = [...this.state.telephones];
newTelephoneData[index][type] = e.target.value;
this.setState({ telephones: newTelephoneData });
}
<select
value={type}
onChange={this.telephoneChangeHandler(index, "type")}
>
// options
</select>
...
<input
value={number}
onChange={this.telephoneChangeHandler(index, "number")}
placeholder={`Telephone No. #${index + 1}`}
/>
Below I've created a few working sandbox demos:
Dual telephone data arrays, single change ("action/reducer") handler, separate add/remove functions, single map pass using index accessor to second array:
Single telephone data array, single change handler, separate add/remove, single map pass:
Use react useReducer, single array, reducer handles add/remove/update, single map pass:
I've included code documentation but if anything is unclear please let me know and I can clean up these demos.
Yes, absolutely, something along:
telephone: {
list: [1,2,3],
type: [“foo”, “bar”, “baz”]
}
I have the following setup:
this.state = {
values: {
id: null,
name: "",
unitName: "",
unitCategory: ""
}
};
and when I change a value in a field in a form I have the following handleChange function:
this.setState({
values: {
[e.target.name]: e.target.value
}
});
the problem is - this removes all other values from the values object in my state and only leaves the one that's being modified (i.e. <input name="name" />).
How could I retain all other values?
Edit
The code now looks like:
this.setState(prevState => {
console.log(prevState.values);
return {
values: {
...prevState.values,
[e.target.name]: e.target.value
}
};
});
The console.log(prevState.values) returns:
{id: 2, name: "Humidity", unitName: "himidity", unitCategory: "%"}
Which is how it should be, but when I spread it in values object, I get:
TypeError: Cannot read property 'name' of null
Use spread syntax to spread the other properties into state:
this.setState(prevState => ({
values: {
...prevState.values,
[e.target.name]: e.target.value
}
}));
This also utilizes the callback with setState to ensure the previous state is correctly used. If your project doesn't support object spread syntax yet, you could use Object.assign:
values: Object.assign({}, prevState.values, {
[e.target.name]: [e.target.value]
})
This does essentially the same thing. It starts with an empty object to avoid mutation, and copies prevState.values's keys and values into the object, then copies the key [e.target.name] and its value into the object, overwriting the old key and value pair.
Also, I'm not sure why you're doing all this:
this.state = {
values: {
id: this.state.values.id ? this.state.values.id : null,
name: this.state.values.name ? this.state.values.name : "",
unitName: this.state.values.unitName ? this.state.values.unitName : "",
unitCategory: this.state.values.unitCategory? this.state.values.unitCategory: "
}
};
When you set initial state in the constructor, just give the initial value, your ternary operator will never give you the true condition:
this.state = {
values: {
id: null,
name: '',
unitName: '',
unitCategory: ''
}
};