Working with Forms in React without libraries
Handling forms in JavaScript could be a difficult task, in this article we will learn how to tame them.
Uncontrolled Input
First we need to talk about uncontrolled inputs, where I say input it's also select or textarea. This is the default state of an input, in this case we do nothing special and let the browser handle the value of it.
function Form() {
const [message, setMessage] = React.useState("");
function handleSubmit(event) {
event.preventDefault();
setMessage(event.target.elements.message.value);
event.target.reset();
}
return (
<>
<p>{message}</p>
<form onSubmit={handleSubmit}>
<input name="message" type="text" />
</form>
</>
);
}
As we can see in the example above we update our state message
with the value of the input after the user submit the form, press enter
, and to reset the input value we just reset the whole form using the reset()
methods of the forms.
This is normal DOM manipulation to read the value and reset it, nothing special of React.
Controlled Input
Now let's talk about the interesting part, a controller input/select/textarea is an element where the value is bound to the state and we need to update the state to update the input value the use see.
function Form() {
const [message, setMessage] = React.useState("");
function handleSubmit(event) {
event.preventDefault();
setMessage("");
}
function handleChange(event) {
setMessage(event.target.value);
}
return (
<>
<p>{message}</p>
<form onSubmit={handleSubmit}>
<input
name="message"
type="text"
onChange={handleChange}
value={message}
/>
</form>
</>
);
}
Our example set the input
value to message
and attached a onChange
event listener we call handleChange
, inside this function we need the event.target.value
where we will receive the new value of the input, which is current value plus what the user typed, and we call setMessage
to update our component state, this will update the content of the p
tag and the value of the input
tag to match the new state.
If we want to reset the input we could call setMessage("")
, as we do in handleSubmit
, and this will reset the state and doing so the input's value and the p
content.
Adding a Simple Validation
Now let's add a simple validation, complex validations are similar but with more rules, in this case we will make the input invalid if the special character _
is used.
function Form() {
const [message, setMessage] = React.useState("");
const [error, setError] = React.useState(null);
function handleSubmit(event) {
event.preventDefault();
setError(null);
setMessage("");
}
function handleChange(event) {
const value = event.target.value;
if (value.includes("_")) setError("You cannot use an underscore");
else setError(null);
setMessage(value);
}
return (
<>
<p>{message}</p>
<form onSubmit={handleSubmit}>
<input
id="message"
name="message"
type="text"
onChange={handleChange}
value={message}
/>
{error && (
<label style={{ color: "red" }} htmlFor="message">
{error}
</label>
)}
</form>
</>
);
}
We create two states, one for the input value and another of the error message. As before inside our handleSubmit
we will reset the message
state to an empty string and additionally we will reset the error
state to null
.
In the handleChange
we will read the new value of the input and see if the underscore is there. In case we found an underscore we will update the error state to the message "You cannot use an underscore"
if it's not there we will set it to null
. After the validation we will update the message
state with the new value.
In our returned UI we will check of the presence of an error
and render a label
with text color red pointing to the input and showing the error message inside. The error is inside a label to let the user click it and move the focus to the input.
Controlling a Textarea
Before I said working with input
and textarea
was similar, and it actually is, let's change the element we render to a textarea
, our code above will continue to work without any other change as we could see below.
function Form() {
const [message, setMessage] = React.useState("");
const [error, setError] = React.useState(null);
function handleSubmit(event) {
event.preventDefault();
}
function handleChange(event) {
const value = event.target.value;
if (value.includes("_")) {
setError("You cannot use an underscore");
} else {
setError(null);
setMessage(value);
}
}
return (
<>
<p>{message}</p>
<form onSubmit={handleSubmit}>
<textarea
id="message"
name="message"
onChange={handleChange}
value={message}
/>
{error && (
<label style={{ color: "red" }} htmlFor="message">
{error}
</label>
)}
</form>
</>
);
}
While usually textarea
is an element with internal content as <textarea>Content here</textarea>
in React to change the value we use the value
prop like an inputs and the onChange
event, making the change between input and textarea similar.
Controlling a Select
Now let's talk about the select
. As with the textarea
you treat it as a normal input
, pass a value
prop with the selected value and listen to value changes with onChange
. The value passed to the select
should match the value of one of the options to show one of them as the currently selected option.
function Form() {
const [option, setOption] = React.useState(null);
const [error, setError] = React.useState(null);
function handleSubmit(event) {
event.preventDefault();
}
function handleChange(event) {
setOption(event.target.value);
}
function handleResetClick() {
setOption(null);
}
function handleHooksClick() {
setOption("hooks");
}
return (
<>
<p>{option}</p>
<form onSubmit={handleSubmit}>
<select onChange={handleChange} value={option}>
<option value="classes">Classes</option>
<option value="flux">Flux</option>
<option value="redux">Redux</option>
<option value="hooks">Hooks</option>
</select>
</form>
<button type="button" onClick={handleResetClick}>
Reset
</button>
<button type="button" onClick={handleHooksClick}>
Hooks!
</button>
</>
);
}
Working with File Inputs
Now to finish let's talk about the file input, this special input can't be controlled, but it's still possible to get some data and save it in the state to show it elsewhere. In the example below we are creating a custom UI for a hidden file input.
function Form() {
const [fileKey, setFileKey] = React.useState(Date.now());
const [fileName, setFileName] = React.useState("");
const [fileSize, setFileSize] = React.useState(0);
const [error, setError] = React.useState(null);
function resetFile() {
setFileKey(Date.now());
setFileName("");
setFileSize(0);
setError(null);
}
function handleChange(event) {
const file = event.target.files[0];
setFileSize(file.size);
if (file.size > 100000) setError("That file is too big!");
else setError(null);
setFileName(file.name);
}
return (
<form>
<label htmlFor="file">
Select a single file to upload. (max size: 100kb)
<br />
{fileName && (
<>
<strong>File:</strong> {fileName} ({fileSize / 1000}kb)
</>
)}
<input id="file" type="file" key={fileKey} onChange={handleChange} style={{ display: "none" }} />
</label>
{error && (
<label style={{ color: "red" }} htmlFor="file">
{error}
</label>
)}
<button type="button" onClick={resetFile}>
Reset file
</button>
</form>
);
}
We listen to the change event and read the file size and name and validate the size of the file, if it's too big we set the error
state to the message "That file is too big!"
, if the file is not that big we will set the error to null
, this let us remove the previous error if the user selected a big file before.
We also have a button to reset the input, since we can't control the state we could use the key
to force React render the input again and resetting it in the process, we use the current date and every time the user click on Reset file
it will get the current date and save it to the fileKey
state and reset it input.