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.