I recently had to refactor a React Form when working with multiple inputs and I thought I'd share how I did it.
This is how my form looks like:
The Problem
- Look at the starter code below
- This
Form
component has 5 input fields in total; 5 different states and 5 differentonChange
inline functions - This is not exactly DRY code 🙀
import React, { useState } from "react";
export default function Form() {
const [newCompany, setCompany] = useState("");
const [newPosition, setPosition] = useState("");
const [newLink, setNewLink] = useState("");
const [newDate, setNewDate] = useState("");
const [newNote, setNewNote] = useState("");
return (
<form>
<input
value={newCompany}
onChange={(e) => setCompany(e.target.value)}
label="Company"
/>
<input
value={newPosition}
onChange={(e) => setPosition(e.target.value)}
label="Job Title"
/>
<input
value={newLink}
onChange={(e) => setNewLink(e.target.value)}
label="Job Link"
/>
<input
type="date"
value={newDate}
onChange={(e) => setNewDate(e.target.value)}
/>
<input
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
label="Note"
/>
<button type="submit"> Submit </button>
</form>
);
}
- Also, if I want to add a
reset
function later, my code will look like this: 🙅🏽♀️
const reset = () => {
setCompany("");
setPosition("");
setNewLink("");
setNewDate("");
setNewNote("");
};
The Solution: Refactoring ✨
Step 1: Add input default values and initialize state
- First, let's add default values to ALL input fields
- How do we do that? We create an object literal with those values and set to empty string
- Then, with the
useState()
React Hook we initialize ourvalues
state with theinitialValues
object -
Important: Remember to add the
value
attribute to every input field with its corresponding value (e.g.values={values.company}
)
const initialValues = {
company: "",
position: "",
link: "",
date: "",
note: "",
};
export default function Form() {
const [values, setValues] = useState(initialValues);
return (
<form>
<input
value={values.company}
onChange={(e) => setCompany(e.target.value)}
label="Company"
/>
//...
Step 2: Handle multiple input change
- The goal here is to handle ALL inputs with a single
onChange
handler - In order to update and keep track of our input fields every time they change, we need to create a
handleInputChange
function (see below) - What's happening here? (quick recap)
- First, we're using object destructuring to get or extract the
name
and thevalue
attributes from our inputs (look at the the comments below - they're equivalent) - Then, we're updating our
values
state object with the existing values by using thesetValues()
function and the spread operator - And finally, we're updating the
value
of the event that was triggered by thatonChange
with this ES6 syntax:[name]: value
- This is a very important step!
We need to add a
name
attribute to our inputs and[name]: value
here means that we're setting a dynamicname
property key (taken from our inputs - e.g.company: e.target.value
) which will be equal to thevalue
of our current input state.
- First, we're using object destructuring to get or extract the
Reference: React Docs
//...
const handleInputChange = (e) => {
//const name = e.target.name
//const value = e.target.value
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
return (
<form>
<input
value={values.company}
onChange={handleInputChange}
name="company" //IMPORTANT
label="Company"
/>
// ...
Step 3: Add handleInputChange
to input fields
- Add the
handleInputChange
function to theonChange
prop of every input field - Look at the final code; this is a much cleaner and manageable form 👌🏽
import React, { useState } from "react";
const initialValues = {
company: "",
position: "",
link: "",
date: "",
note: "",
};
export default function Form() {
const [values, setValues] = useState(initialValues);
const handleInputChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
return (
<form>
<input
value={values.company}
onChange={handleInputChange}
name="company"
label="Company"
/>
<input
value={values.position}
onChange={handleInputChange}
name="position"
label="Job Title"
/>
// ... Rest of the input fields
<button type="submit"> Submit </button>
</form>
);
}
I hope it was useful. All comments and feedback are welcome! 🎊
Top comments (23)
Hi this method works fine for multiple input but just with normal input fields. What if their are some booleans and arrays??? E.g In vue.js I have the following data model which change at some point in time, which in that case two way data binding make it way easier for us.
Thank you so much!! I'm learning about controlled forms, and this has been incredibly helpful!
Amazing perofrmance. Very thanks!
Hi, just wanted to ask ,
since we are changing the state in handleInputChange
therefore, every time we will type in something the whole component will re-render since the state is changing
so isn't it an expensive approach in respect to performance?
This was so insightfull. But i have one quetion. how then would you reset the input fields upon form submission?. is it as simple as calling e.g setFormFields ( initialState )
Here my assumption is that initialState is an object e.g
const initialState={
someInput: ' ',
anotherInput: ' ',
}
very good , but I don't like it. Now you have to maintain the names of the inputs, which is a string. It's a bug waiting to happen.
I suggest using this solution: stackoverflow.com/a/47707719/1079002
btw, do you have solution for nested objects? I need that, thanks!
Amazing Deborah ! ☺️☺️
Can we map the input filed using an array of states. So that we can reduce the code size. Is it possible to do that?
Thanks for providing with an useful information in a synchronize manner ...
Amazing perofrmance. Very thanks!
Thank you so much, I was handling this terrible, but with your handleChange I was able to refactor the entire event! fav*