setState only setting last input when using object as state - javascript

Im trying to create a form with React. This form uses a custom Input component I created various times. In the parent form Im trying to get a complete object with all names and all values of the form:
{inputName: value, inputName2: value2, inputName3: value3}
For this, I created a 'component updated' hook, that calls the function property onNewValue to send the new value to the parent (two way data binding):
useEffect(() => {
if (onNewValue) onNewValue({ name, value });
}, [value]);
The parent form receives the data in the handleInputChange function:
export default () => {
const [values, setValues] = useState({});
const handleInputChange = ({
name,
value
}: {
name: string;
value: string | number;
}): void => {
console.log("handleInputChange", { name, value }); // All elements are logged here successfully
setValues({ ...values, [name]: value });
};
return (
<>
<form>
<Input
name={"nombre"}
required={true}
label={"Nombre"}
maxLength={30}
onNewValue={handleInputChange}
/>
<Input
name={"apellidos"}
required={true}
label={"Apellidos"}
maxLength={60}
onNewValue={handleInputChange}
/>
<Input
name={"telefono"}
required={true}
label={"Teléfono"}
maxLength={15}
onNewValue={handleInputChange}
/>
<Input
name={"codigoPostal"}
required={true}
label={"Código Postal"}
maxLength={5}
onNewValue={handleInputChange}
type={"number"}
/>
</form>
State of values: {JSON.stringify(values)}
</>
);
};
This way all elements from all inputs should be set on init:
{"codigoPostal":"","telefono":"","apellidos":"","nombre":""}
But for some reason only the last one is being set:
{"codigoPostal":""}
You can find the bug here:
https://codesandbox.io/s/react-typescript-vx5py
Thanks!

The set state process in React is an asynchronous process. Therefore even if the function is called, values has not updated the previous state just yet.
To fix, this you can use the functional version of setState which returns the previous state as it's first argument.
setValues(values=>({ ...values, [name]: value }));

useState() doesn't merge the states unlike this.setState() in a class.
So better off separate the fields into individual states.
const [nombre, setNombre] = useState("")
const [apellidos, setApellidos] = useState("")
// and so on
UPDATE:
Given setValue() is async use previous state during init.
setValues((prevState) => ({ ...prevState, [name]: value }));

The updated and fixed code, look at:
https://codesandbox.io/s/react-typescript-mm7by
look at:
const handleInputChange = ({
name,
value
}: {
name: string;
value: string | number;
}): void => {
console.log("handleInputChange", { name, value });
setValues(prevState => ({ ...prevState, [name]: value }));
};

const [ list, setList ] = useState( [ ] );
correct:
setList ( ( list ) => [ ...list, value ] )
avoid use:
setList( [ ...list, value ] )

Related

Storing state of child components with clean code

Short Explanation
I just want to get the data from the textboxes and send them to the server. In jquery I would just list out all my textboxes and get the value and form the json and send it to the server
Please see example code
https://codesandbox.io/s/xenodochial-euclid-3bl7tc
const EMPTY_VALUE = "";
const App = () => {
const [valueFirstName, setValueFirstName] = React.useState(EMPTY_VALUE);
const [valueMiddleName, setValueMiddleName] = React.useState(EMPTY_VALUE);
const [valueLastName, setValueLastName] = React.useState(EMPTY_VALUE);
return (
<div>
First Name:
<Work365TextBox value={valueFirstName} setValue={setValueFirstName} />
<div>{valueFirstName}</div>
Middle Name:
<Work365TextBox value={valueMiddleName} setValue={setValueMiddleName} />
<div>{valueMiddleName}</div>
Last Name:
<Work365TextBox value={valueLastName} setValue={setValueLastName} />
<div>{valueLastName}</div>
</div>
);
};
Problem
The current code has a label for first name, middle name, and last name and components to contain the textboxes and then the state of the component is stored in the parent.Then the parent takes the state and displays it below the textbox. So the solution works great. But the code is messy
Question
If I have a form that asks for 20 values what is a cleaner way to handle this ? I have tried to do this with by defining a object as json and calling a method when the value in each textbox changes but then I need to have a method for each textbox I have on the screen and it doesn't seem very clean. How would you solve this problem in a clean way ? Preferably I want to be able to have 50 textbox components but just call one method to update state.
the object solution you mentioned is a great way.
one thing to add is that you can pass a name prop to your input and then in the onChange method you can access it via event.target.name
that way you can dynamically update your object
something like this
const onChangeHandler = (event) => {
const name = event.target.name;
const value = event.target.value;
setState((prev) => ({ ...prev, [name]: value }));
};
another solution is to define it with useReducer but that will need a extra code.
I would just use an array like that:
// one item for each textbox
const textboxes = [
{
// human-readable label
label: "First name",
// unique key for data access
key: "firstName"
},
{
label: "Middle name",
key: "middleName"
},
{
label: "Last name",
key: "lastName"
}
];
A state like this:
const [values, setValues] = React.useState(
// converts array to record-like object
Object.fromEntries(textboxes.map((box) => [box.key, ""]))
);
const handleChange = (key, value) => {
setValues((values) => ({ ...values, [key]: value }));
};
And render it like this:
<div>
{textboxes.map((box) => (
<>
{box.label}:
<Work365TextBox
value={values[box.key]}
setValue={(value) => handleChange(box.key, value)}
/>
<div>{values[box.key]}</div>
</>
))}
</div>
Full example

Rendering array data with a condition

I need to make a large number of inputs and transfer this data to the server, I decided that the best solution would be to write all the options of these inputs into an array of objects, but I ran into the fact that I can’t get all my inputs to work. help me please
const test = [
{id: 1,state: 'city'},
{id: 2,state: 'language'},
{id: 3,state: 'brand'},
{id: 4,state: 'shop'},
]
const Auth = () => {
const [description, setDescription] = useState({city: "", language: "", brand: "", shop: ""});
const handleClick = async (event: any) => {
await store.update(description.city, description.brand);
};
const update = async (e: ChangeEvent<HTMLInputElement>) => {
setDescription({
...description,
city: e.target.value
});
};
return (
<>
{test.map(({ state, id}) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description.city}
onChange={update}
/>
))}
<Button
className={styles.saveButton}
variant="contained"
color="inherit"
id="login"
onClick={handleClick}
>
Save
</Button>
</>
)
}
You send to TextField description.city for every input. The correct props are like so:
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={update}
/>
See the change in the value prop.
Also, you only update city in the update function. You have to make it so that the update function adapts to what values you pass to it. If you pass the city then it should update the city, if the language then the language and so on.
Overall this is not a good way to implement inputs. I just suggest you do them one by one and send to each TextField its corresponding value and a separate setState for each one.
But just for the sake of the example. The way you can do it is by passing the state value to the Update function.
So your function will look like this:
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription((description) => {
...description,
[state]: e.target.value
});
};
Now you just need to make sure that in the TextField component when you call onChange, you pass to it the event e and state which you have received from props.
Note: If you want to use the value of a state variable in the setState itself, pass to it a callback function like I did in the setDescription
if you want to make it dynamic you would have to send the variable to save to your update method and retrieve your value with description[state]
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e)=>update(e, state)}
/>
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription({
...description,
[state]: e.target.value
});
};
I think first and foremost you need your configuration data to try and closely match the elements you're building. So instead of { id, state } use { id, type, name }.
(This may not have a huge effect on your example because you're specifically using a TextField component, but if you were using native HTML controls you could add in different input types like number, email, date etc, and your JSX could deal with it easily.)
Second, as I mentioned in the comments, you don't need for those functions to be async - for example, there's no "after" code in handleClick so there's no need to await anything.
So here's a working example based on your code. Note: I've stripped out the Typescript (because the snippet won't understand the syntax), and the references to the UI components you're using (because I don't know where they're from).
const { useState } = React;
// So, lets pass in out inputs config
function Example({ inputs }) {
// I've called the state "form" here as it's a little
// more meaningful
const [form, setForm] = useState({});
// `handleSave` is no longer `async`, and for the
// purposes of this example just logs the updated
// form state
function handleSave() {
console.log(form);
// store.update(form);
}
// Also no longer `async` `handleChange` destructures
// the name and value from the changed input, and updates
// the form state - a key wrapped with `[]` is a dynamic key
// which means you can use the value of `name` as the key value
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
// In our JSX we destructure out the id, name, and
// type properties from each input object in the config
// and apply them to the various input element properties.
return (
<div>
{inputs.map(input => {
const { id, name, type } = input;
return (
<input
key={id}
type={type}
name={name}
placeholder={name}
value={form[name]}
onChange={handleChange}
/>
);
})}
<button onClick={handleSave}>Save</button>
</div>
);
}
// Our updated config data
const inputs = [
{ id: 1, type: 'text', name: 'city' },
{ id: 2, type: 'text', name: 'language' },
{ id: 3, type: 'text', name: 'brand' },
{ id: 4, type: 'text', name: 'shop' }
];
ReactDOM.render(
<Example inputs={inputs} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Pass both the key that you want to update and the value to the update function:
const update = (key: string, value: string) => {
setDescription({
...description,
[key]: value,
});
};
{test.map(({ state, id }) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
update(state, e.target.value)
}
/>
))}

How do I Map several controlled input checkboxes from an array in React?

I'm trying to make a number of controlled checkboxes from an array like so
const subjects = [
{
name: 'WebDev',
label: 'Web Development',
},
{
name: 'GraphicDesign',
label: 'Graphic Design',
},
{
name: 'Engineering',
label: 'Engineering',
},
];
I want to make them controlled, so for the checked attribute, I need it to be controlled by state. But how do I loop through them and access the correct object in state each time? I guess I could maybe parse the subject.name string, but surely that is not good practice? Indeed, I could perhaps make a matching array in my state where the indexes match up, but I don't feel like that is scalable and relies heavily on correct order.
{subjects.map((subject, i) => (
<div className="w-full sm:w-auto" key={i}>
<label className="flex content-center p-3">
<input type="checkbox" name={subject.name} value={subject.name} onChange={onInputChange} checked={????}/>
<span className="ml-3">{subject.label}</span>
</label>
</div>
))}
I also have other inputs like this. For them, it is easy because I can just use state.name to control them.
<input id="name" type="text" value={state.name} onChange={onInputChange} name="name" required/>
I then use a generalised function to update the inputs. This takes use of the useState() Hook
const onInputChange = e => {
const item = e.target.name;
const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
setState({
...state,
[item]: value,
})
}
What is the best practice in React for approaching something like this? If I could contain the subjects in my state as shown below it would be good.
Should I maybe be ditching the seperate array and containing all the information in state?
const [state, setState] = useState({
name:'',
email: '',
message: '',
checkedSubs: {
WebDev: false,
GraphicDesign: false,
Engineering: false
},
emailSent: false
})
Thanks!
Use multiple states to track the state of the subjects and your general data. Give each subject an extra property called check which should hold a boolean value. This value will determine the checked state of your checkboxes.
In the onInputChange function update the subjects state with the new checked state of the input.
Add a useEffect hook which has a dependency: the subjects array. Whenever the value of subjects changed it will trigger the logic inside of the useEffect callback function. In that function update your general state based on the values in the subjects array.
The end result will be that the subjects array will determine your inputs. Whenever an input is changed the subjects state will be updated. Then when the subjects changes, the inputs will be rerendered and the values in data will be updated based on the change.
const ExampleComponent = () => {
const [ subjects, setSubjects ] = useState([
{
name: 'WebDev',
label: 'Web Development',
checked: false
},
{
name: 'GraphicDesign',
label: 'Graphic Design',
checked: false
},
{
name: 'Engineering',
label: 'Engineering',
checked: false
},
]);
const [ data, setData ] = useState({
name:'',
email: '',
message: '',
checkedSubs: {
WebDev: false,
GraphicDesign: false,
Engineering: false
},
emailSent: false
});
const onInputChange = e => {
const item = e.target.name;
const isChecked = e.target.checked;
const updatedSubjects = subjects.map(subject => {
if (subject.name === item) {
return {
...subject,
checked: isChecked
}
}
return subject;
});
setSubjects(updatedSubjects);
};
useEffect(() => {
setData({
...data,
checkedSubs: subjects.reduce((acc, cur) => {
acc[cur.name] = cur.checked;
return acc;
}, {})
});
}, [subjects]);
return (
subjects.map(({ name, label, checked }, i) => (
<div className="w-full sm:w-auto" key={i}>
<label className="flex content-center p-3">
<input type="checkbox" name={name} value={name} onChange={onInputChange} checked={checked}/>
<span className="ml-3">{label}</span>
</label>
</div>
))
)
}
Universal onInputChange
Your current onInputChange handler updates a property in state based on the name property of the input. One of the easiest ways for you to implement this is to let this handler update the checkboxes as it would any other field.
This means that your subjects would become top-level properties of state. This is fine as long as there is no chance that a subject name is the same as a form field like "email". You can rearrange the state however you want into some other variable.
You can map to your example format:
// can't use variable subjects because it's already defined
const {name, email, message, emailSent, ...subjectBooleans} = state;
const formatted = {name, email, message, emailSent, subjects: subjectBooleans}
You can get an array with the names of the checked subjects only:
const checkedSubjects = subjects
.filter(o => state[o.name])
.map( o => o.name );
I'm not sure where the subjects array is coming from and if it's a constant or something that might change. You can just omit these properties from your initial state and assume that undefined means false/unchecked. We see if a subject is checked by looking at state[subject.name] and we can explicitly cast to a boolean with !! to better handle this undefined scenario.
const MyForm = ({ subjects }) => {
// removed initial values for subjects
// you could set these, but they aren't required
const [state, setState] = useState({
name: "",
email: "",
message: "",
emailSent: false
});
// no changes to this function
const onInputChange = (e) => {
const item = e.target.name;
const value =
e.target.type === "checkbox" ? e.target.checked : e.target.value;
setState({
...state,
[item]: value
});
};
return (
<form>
<input
id="name"
type="text"
value={state.name}
onChange={onInputChange}
name="name"
required
/>
{/* ... other inputs ... */}
{subjects.map((subject, i) => (
<div className="w-full sm:w-auto" key={i}>
<label className="flex content-center p-3">
<input
type="checkbox"
name={subject.name}
value={subject.name}
onChange={onInputChange}
checked={!!state[subject.name]}
/>
<span className="ml-3">{subject.label}</span>
</label>
</div>
))}
</form>
);
};

How to update a text field without using the useState hook?

I have a component structured like this:
export const EditProductDescription = ({ product, lang, onChange, onSubmit }: ProductDescriptionInterface) => {
let editedProduct = { ...product };
const handleChange = (field: string, value: string, multiLanguage: boolean) => {
multiLanguage
? { ...editedProduct, description: { [field]: { [lang]: value } } }
: { ...editedProduct, description: { [field]: value } };
console.log(editedProduct);
};
return (
<TextArea
id="description"
placeholder="Description"
value={editedProduct.details.description.text[lang] || ' '}
label="Description"
onChange={e => handleChange('text', e.target.value, true)}
/>
);
};
My Product model ist structured like this:
export class Product {
...
details: {
...
description: {
...
text: {de?: string, en?: string}
...
}
...
}
...
}
I would like to update the description of the product or have the opportunity to edit it.
With my current approach, the product object is not updated as desired. Accordingly, the value in the text field is not updated either.
My handleChange method receives the field I want to edit as a parameter. The multiLanguage parameter is also used to specify whether this should be created in multiple languages. Multilingualism is important here.
How do I have to adapt my handleChange method so that I can get the result I want without using the useState hook? Is that even possible?
What am I doing wrong?

Calling setState in callback of setState generate a weird bug that prevents input's value to be updated by onChange

I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?

Categories