Something almost any application or website will include will undoubtedly be a form in some shape or manner. Whether it be simply for user login or to modify profile data or for a search function a form is simply a core building block in development. Chances are if you're reading this you have at least some idea as how to build a form out with HTML. This post is to demonstrate how a form is handled differently in a React application and how to build it out so it functions within the single page application.
Why is it different in React?
Generally inputs and form elements maintain their own state on a web page this is how you're able to type into them and they maintain the script. React needs access to all of the state on the page including form data. So the forms state and the applications state needs to be the same thing. That's where building out how your form is handled differs in React.
Controlled Components is how its done. What do I mean by that? Well the same way react is rendering the form onto the page it is also in control of rendering the users input onto the form. This is done through the use of connecting the users input to the state of the form component.
A Demonstration
From this point I'll walk you through how to build out a basic dog form component that takes in name, age, and breed. I'll be personally using functional arrow components and the useState hook built in to React as opposed to a class component to handle state.
Okay so first things first lets just create our component and make sure to bring in useState.
import React, { useState } from 'react';
const ReactForm = () => {
}
export default ReactForm;
Next thing we can do is build out our form in our return statement with labels, inputs, and a submit.
return (
<div>
<form>
<label htmlFor="name">Name:</label>
<input type="text" name="name" />
<label htmlFor="age">Age:</label>
<input type="text" name="age" />
<label htmlFor="name">Breed:</label>
<input type="text" name="breed" />
<input type="submit" value="submit" />
</form>
</div>
)
Looks like a pretty standard HTML form at this point right? Well this is where it will start to differ and we will begin tying the inputs to the state of the component.
We will need state to store the input values for name, age, and breed since that's the case. Lets set that up in our component with the useState hook.
const [ name, setName ] = useState("");
const [ age, setAge ] = useState("");
const [ breed, setBreed ] = useState("");
Alright so now that we have a place in our component to store the inputs state we can then tie associate the value of the input to that state variable. Do this for each input. It should look like this.
<input type="text" name="name" value={name}/>
Now that the value of the input is associated with the state variable and the state variable is an empty string "" you'll notice in your form you can no longer type into the input box. Go ahead try it.
That's a good sign. Means your input is associated to the components state. The problem at hand now is how do we allow for actual user input. Well it's associated with state so we have to change state right?
Let's create a function for that.
const handleChange = ({ target }) => {
switch(target.name){
case "name":
setName(target.value);
break;
case "age":
setAge(target.value);
break;
case "breed":
setBreed(target.value);
break;
default:
return;
}
}
Woah woah woah what's going on in there? Let's break it down
A handleChange function is created. It takes in an event. I've chosen to deconstruct the target out from the event because what we're looking at is the event.target which in turn will be the input tag this function is associated with
Since there are multiple inputs I've set up a switch statement that will consider the targets name value for each case.
Dependent on the value of the target.name the switch statement will use our earlier created hook to change the values state.
Now that the function for handling state change is created we just have to associate it with each input tag by assigning an onChange to our function like so:
<input type="text" name="name" value={name} onChange={handleChange}/>
Voila! Now that the change handler is associated with the input you should successfully be able to type into the input boxes. What does that mean then? That your changing the state of the component by typing into these inputs, and more importantly that the inputs state and the components state are one and the same.
Okay so what else do we have to do with the form? Probably work on making sure you can submit the form. Lets create a handleSubmit function for that. I will be making it so the form makes a POST request to a dummy backend through the use of https://httpbin.org. It's an excellent resource for testing a request without it actually going anywhere.
const handleSubmit = (event) => {
event.preventDefault();
let dog = {
name,
age,
breed
}
fetch("https://httpbin.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dog),
})
.then((response) => response.json())
.then(console.log);
}
Let's walk through this one.
A handleSubmit function is created that takes in an event as an argument.
First we use preventDefault() on our event because what does a form generally do on submit? Refresh or redirect and that's not what we would like to do with a single page application.
A dog object is created and we can assign each of its key value pairs to the state variables that store them.
A fetch POST request is made to the dummy backend and submits that previously mentioned dog object with our states values.
After a response is received and turned into JSON it is then locked to the console.
Alright now that we have determined the functionality of our submit let's associate it with our form by assigning our function to onSubmit like so:
<form onSubmit={handleSubmit}>
Now when you fill out the inputs and hit submit it will make a POST request that will in turn send a response which will then be logged to your console. Test it out.
Success. Your form is functional.
One last thing you can do is since the inputs are tied to state (Have I mentioned that yet?) is to clear the inputs on a submit.
The way I handled that is created a super simple helper function:
const clearForm = () => {
setName("");
setAge("");
setBreed("");
}
And invoked it during the then chain on my handleSubmit:
.then(clearForm())
.then((response) => response.json())
.then(console.log);
You should now have a controlled form component that allows for form submission and the form will clear on submit. How about that. Anyways that will be it from me today I hope you picked something up and as always feel free to reach out to me about any questions or comments. Happy Coding!
Top comments (6)
life's easier if you just use a single state object for your whole form, and a changeHandler that uses es6 dynamic properties. the switch and custom useStates get annoying real quick.
not the best way to approach formstate
Thanks for taking the time to read the article! I understand where you're coming from but opted for this approach purely by preference.
I appreciate your input.
A link so i can learn more about this other approach please?
a simple example, to get you started
this will allow you to implicitly handle change for any form input, via its name attribute.
note that for fields that dont use
value
(like checkboxes) you'll have to do some more handlingyou can also pass in a default state to the
useState
, or type it in typescript like that.Aw ok thanks for replied
Great article! I love approach using onSubmit instead onChange.
Sometimes, uncontrolled components are easier to manipulate.