A form is an integral component of every website. Whether you’re registering for a service, signing into your email, or sending messages to your friends, you’ve always needed a form for these actions. Without forms, websites and web applications may have been read-only, offering only a one-way method of communication where users cannot send messages or data to the system.
In this tutorial, you’ll learn how to create and validate forms in React, ensuring that users provide data in a specified format.
Additionally, you’ll learn how to use validation schema libraries, such as Yup and Zod, and other form management libraries, like React Hook Form and Formik.
A controlled form is one where React states manage and handle the form values – allowing you to perform various actions or modify the input as the user enters it.
On the other hand, in an uncontrolled form, the form components manage their values. The data are read-only until the user submits the form, making it more suitable for creating simple forms.
Let’s create a Contact form that accepts a name, email, and message using both methods.
import { useState } from "react";
const App = () => {
//👇🏻 states for modifying the form inputs
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
//👇🏻 executes when a user submits the form
const handleSubmit = (event) => {
event.preventDefault();
console.log({ name, email, message }); setName(""); setEmail(""); setMessage("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Contact Us</h2>
<label htmlFor='name'>Name</label>
<input
type='text'
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor='message'>Message</label>
<textarea
rows={5}
id='message'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type='submit'>SEND</button>
</form>
</div>
);
};
From the code snippet above, the React states manage and store the form values, enabling us to control the state values using the `setState` function and perform various actions as the user provides the input.
const App = () => {
//👇🏻 executes when a user submits the form
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const object = Object.fromEntries(formData);
console.log(object);
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Contact Us</h2>
<label htmlFor='name'>Name</label>
<input type='text' id='name' name='name' />
<label htmlFor='email'>Email</label>
<input type='email' id='email' name='email' />
<label htmlFor='message'>Message</label>
<textarea rows={5} id='message' name='message' />
<button type='submit'>SEND</button>
</form>
</div>
);
};
The code snippet above uses the JavaScript FormData object to collect the form inputs. Unlike the previous example, you cannot modify the user’s input directly because the form elements manage the inputs. This method is called an Uncontrolled Form.
An uncontrolled form can also be created using the React useRef hook.
It assigns a reference to each input field to enable us to access their values.
import { useRef } from "react";
const App = () => {
const nameRef = useRef(null);
const emailRef = useRef(null);
const messageRef = useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
// Access input values directly using refs
const name = nameRef.current.value;
const email = emailRef.current.value;
const message = messageRef.current.value;
console.log({ name, email, message });
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Contact Us</h2>
<label htmlFor='name'>Name</label>
<input type='text' id='name' ref={nameRef} />
<label htmlFor='email'>Email</label>
<input type='email' id='email' ref={emailRef} />
<label htmlFor='message'>Message</label>
<textarea rows={5} id='message' ref={messageRef} />
<button type='submit'>SEND</button>
</form>
</div>
);
};
Forms play a crucial role in web applications, but they can be a source of harm to your application. Users can enter wrongly formatted data, and attackers could exploit forms to steal sensitive information or crash the application.
To prevent this occurrence, you need to validate form inputs. In modern applications, form inputs are validated both on the client and server side of the application. This process ensures that inputs are of the desired format, preventing harmful inputs or attacks from being processed by the application.
There are various ways of validating form inputs in React. In the upcoming sections, you’ll learn about a few of these methods.
HTML form elements provide some default attributes that enable us to validate form inputs before saving them within the application.
A few of them are:
Consider the code snippet below:
import { useState } from "react";
const App = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
//👇🏻 executes when a user submits the form
const handleSubmit = (event) => {
event.preventDefault();
console.log({ username, password, email });
alert("Form Submitted ✅");
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
name='username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={6}
/>
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
name='email'
value={email}
required
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
name='password'
value={password}
required
minLength={8}
onChange={(e) => setPassword(e.target.value)}
/>
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
The code snippet above ensures the user provides a username, email, and password before submitting the form.
The form accepts a username with a minimum of six characters, an email, and a password with a minimum of eight characters.
Another method of validating form inputs is by creating custom JavaScript functions that check if the input is of the desired format before submitting the form.
In this case, you’ll have to check for various errors and display error messages for each case.
Consider the code snippet below:
import { useState } from "react";
const App = () => {
//👇🏻 state for each input
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
//👇🏻 state for error messages
const [errorMessage, setErrorMessage] = useState({
username: "",
email: "",
password: "",
});
// 👇🏻 Function to update error messages
const updateErrorState = (fieldName, message) => {
setErrorMessage((prevState) => ({
...prevState,
[fieldName]: message,
}));
};
//👇🏻 function for validating inputs
const validateInput = (data) => {
const { value, name } = data;
if (name === "username") {
if (value.trim() === "") {
updateErrorState(name, "Username is required");
} else if (value.length < 6) {
updateErrorState(name, "Username must be at least 6 characters");
} else {
updateErrorState(name, "");
}
} else if (name === "email") {
if (value.trim() === "") {
updateErrorState(name, "Email is required");
} else {
updateErrorState(name, "");
}
} else {
if (value.trim() === "") {
updateErrorState(name, "Password is required");
} else if (value.length < 8) {
updateErrorState(name, "Password must be at least 8 characters");
} else {
updateErrorState(name, "");
}
}
};
//👇🏻 submits the form
const handleSubmit = (e) => {
e.preventDefault();
if (
Object.values(errorMessage).some((message) => message !== "") ||
!email ||
!password ||
!username
) {
alert("Invalid Credentials ❌");
return;
}
alert("Form Submitted ✅");
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
name='username'
value={username}
onChange={(e) => {
setUsername(e.target.value);
validateInput(e.target);
}}
/>
{errorMessage.username && <p>{errorMessage.username}</p>}
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
name='email'
value={email}
onChange={(e) => {
setEmail(e.target.value);
validateInput(e.target);
}}
/>
{errorMessage.email && <p>{errorMessage.email}</p>}
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
name='password'
value={password}
onChange={(e) => {
setPassword(e.target.value);
validateInput(e.target);
}}
/>
{errorMessage.password && <p>{errorMessage.password}</p>}
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
The validInput function ensures the user’s input adheres to the specified constraints before submitting the form. Otherwise, it displays the necessary error message to the user.
The function validates that the username, email, and password are not empty, the username contains at least six characters, and the password has at least eight characters.
Yup is a simple JavaScript schema validator that provides multiple functions to enable us to validate whether an object matches a particular set of rules.
It provides a simple and declarative way to define validation rules and ensures that the form inputs adhere to them.
To validate forms with Yup, you need to install the package by running the code snippet below:
npm install yup
Next, create a schema for the form data. The schema describes the shape, constraints, and data type of the values to be retrieved from the form. Yup validates this schema against the user’s input to ensure that the rules stated within the schema are adhered to.
For example, the schema for a form containing an email, a username with a minimum of six characters, and a password of at least eight characters is shown below:
import { object, string } from "yup";
//👇🏻 user schema with Yup
const userSchema = object().shape({
username: string()
.required("Username is required")
.min(6, "Username must be at least 6 characters"),
email: string().email("Invalid Email").required("Email is required"),
password: string()
.required("Password is required")
.min(8, "Password must be at least 8 characters"),
});
The userSchema object describes the form inputs.
For instance, the username is required and must be a string with a minimum of six characters. The string arguments within the function are the errors displayed when an input does not match the specific rule.
Let’s examine the complete code for the form:
import { useState } from "react";
import { object, string } from "yup";
const App = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const [errorMessage, setErrorMessage] = useState({
username: "",
email: "",
password: "",
});
//👇🏻 user schema with Yup
const userSchema = object().shape({
username: string()
.required("Username is required")
.min(6, "Username must be at least 6 characters"),
email: string().email("Invalid Email").required("Email is required"),
password: string()
.required("Password is required")
.min(8, "Password must be at least 8 characters"),
});
//👇🏻 submits the form
const handleSubmit = async (e) => {
e.preventDefault();
try {
await userSchema.validate(
{ username, email, password },
{ abortEarly: false }
);
console.log({ email, username, password });
setErrorMessage({ username: "", email: "", password: "" });
alert("Form Submitted ✅");
} catch (error) {
//👇🏻 update the errors
const errors = error.inner.reduce((accumulator, currentValue) => {
accumulator[currentValue.path] = currentValue.message;
return accumulator;
}, {});
setErrorMessage(errors);
return;
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
name='username'
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
{errorMessage.username && <p>{errorMessage.username}</p>}
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
name='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errorMessage.email && <p>{errorMessage.email}</p>}
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
name='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errorMessage.password && <p>{errorMessage.password}</p>}
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
The code snippet above compares the user’s input with the schema when the user submits the form and displays the error message if the form inputs do not match the rules.
Zod is a TypeScript-first schema declaration and validation library. It enables you to define the schema for your form inputs using concise functions and validates the user inputs against the schema, just like Yup.
You can install Zod by running the code snippet below:
npm install zod
Create a schema for the form inputs.
It describes the form inputs and ensures that they follow the rules.
import { z, ZodError } from "zod";
//👇🏻 user schema with Zod
const schema = z.object({
username: z.string().min(6, {
message: "Username must be at least 6 characters",
}),
email: z.string().email({ message: "Invalid email address" }),
password: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
});
The code snippet above ensures that all the values are string and not empty, the username is at least six characters, the email is valid, and the password is a minimum of eight characters. Otherwise, it returns the error message within the functions.
Let’s examine the complete code for the form:
import { useState } from "react";
import { z, ZodError } from "zod";
const App = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const [errorMessage, setErrorMessage] = useState({
username: "",
email: "",
password: "",
});
//👇🏻 user schema with Zod
const schema = z.object({
username: z.string().min(6, {
message: "Username must be at least 6 characters",
}),
email: z.string().email({ message: "Invalid email address" }),
password: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
});
//👇🏻 submits the form
const handleSubmit = (e) => {
e.preventDefault();
try {
//👇🏻 validates the inputs
schema.parse({ username, email, password });
console.log({ email, username, password });
setErrorMessage({ username: "", email: "", password: "" });
alert("Form Submitted ✅");
} catch (error) {
//👇🏻 updates error message
if (error instanceof ZodError) {
const newFormErrors = {};
error.errors.forEach((err) => {
const fieldName = err.path[0];
newFormErrors[fieldName] = err.message;
});
setErrorMessage(newFormErrors);
}
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
name='username'
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
{errorMessage.username && <p>{errorMessage.username}</p>}
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
name='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errorMessage.email && <p>{errorMessage.email}</p>}
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
name='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errorMessage.password && <p>{errorMessage.password}</p>}
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
When a user submits the form, Zod validates the form inputs and returns the error messages for invalid fields.
However, you’ll notice that in Yup and Zod, validation occurs when the user submits the form. How can we validate the inputs as the user enters them?
In the upcoming section, you’ll learn how to use some advanced yet simple form management libraries.
React Hook Form is a popular library that enables us to build simple, scalable, and highly performant forms. It manages the form inputs, enforces validation, and displays the necessary errors when necessary.
React Hook Form also provides several features, such as a dev tool for monitoring form inputs during development, HTML validation methods, the ability to add custom and regex pattern validation, and support for schema validation libraries, such as Yup or Zod, enabling us to build highly functional and secured forms.
To use React Hook Form, install its package by running the code snippet below.
npm install react-hook-form
Let’s create a signup form using the React Hook Form library:
import { useForm } from "react-hook-form";
const App = () => {
const { register, handleSubmit, formState } = useForm();
const { errors } = formState;
//👇🏻 submits the form
const onSubmit = (data) => {
console.log("Form Submitted", data);
alert("Form Submitted ✅");
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
{...register("username", {
required: "Username is required",
minLength: {
value: 6,
message: "Username must be at least 6 characters",
},
})}
/>
<p>{errors.username?.message}</p>
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
{...register("email", {
required: "Email is required",
pattern: {
value: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
message: "Invalid email address",
},
})}
/>
<p>{errors.email?.message}</p>
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
{...register("password", {
required: "Password is required",
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
})}
/>
<p>{errors.password?.message}</p>
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
The code snippet uses React Hook Form features such as form data management, form validation, form submission, and error display.
React Hook Form gives you complete control of your form and allows you to add various schema validation libraries to build highly secured and complex forms.
In the upcoming sections, you’ll learn how to add Yup and Zod to React Hook Form.
You can extend React Hook Form using its Resolvers package. It enables you to add any validation library to the React Hook Form. Therefore, install Yup and the Resolvers package by running the code snippet below.
npm install @hookform/resolvers yup
Create the form schema using Yup as shown below:
import * as yup from "yup";
const schema = yup
.object()
.shape({
username: yup
.string()
.required("Username is required")
.min(6, "Username must be at least 6 characters"),
email: yup.string().required("Email is required"),
password: yup
.string()
.required("Password is required")
.min(8, "Password must be at least 8 characters"),
})
.required();
Finally, add the schema to the useForm hook using the Yup resolver. It validates the form inputs and ensures that it matches the schema.
import { yupResolver } from "@hookform/resolvers/yup";
const App = () => {
const { register, handleSubmit, formState } = useForm({
resolver: yupResolver(schema),
});
const { errors } = formState;
//👇🏻 submits the form
const onSubmit = (data) => {
console.log("Form Submitted", data);
alert("Form Submitted ✅");
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input type='text' id='username' {...register("username")} />
<p>{errors.username?.message}</p>
<label htmlFor='email'>Email</label>
<input type='email' id='email' {...register("email")} />
<p>{errors.email?.message}</p>
<label htmlFor='password'>Password</label>
<input type='password' id='password' {...register("password")} />
<p className='text-red-500 mb-4 '>{errors.password?.message}</p>
<button type='submit'>REGISTER</button>
</form>
</div>
);
};
You can add Zod to the React Hook Form using the Resolvers package. Install Zod and the Resolvers package by running the code snippet below.
npm install @hookform/resolvers zod
Create the form schema using Zod as shown below:
import * as z from "zod";
const schema = z.object({
username: z.string().min(6, {
message: "Username must be at least 6 characters",
}),
email: z.string().email({ message: "Invalid email address" }),
password: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
});
Finally, add the schema to the useForm hook using the Zod resolver. Zod provides the required schema for the form, and React Hook Form ensures that the provided inputs conform to the schema before submitting the form.
Congratulations! You’ve learnt how to create flexible and secure forms using React Hook Form, and you’ve also learnt how to extend it to accommodate various schema validation libraries, like Yup and Zod.
Formik is a simple library that manages form data, handles form submission and validation, and provides feedback with error messages. It provides a simple way of working with forms using utilities and in-built components.
Like React Hook Form, Formik is a flexible library that allows you to extend its features using schema validation libraries like Yup and Zod.
Run the code snippet below to install Formik
npm install formik
Like the useForm hook in React Hook Form, Formik provides a hook called – the useFormik hook. It enforces data validation, handles form submission, and manages the user’s input.
The useFormik hook accepts an object containing the initial values of the form fields, the onSubmit function, and the validation function.
import { useFormik } from "formik";
const formik = useFormik({
initialValues: {
//👇🏻 form default input
username: "",
email: "",
password: "",
},
//👇🏻 handles form submission
onSubmit: (values) => {
console.log(values);
},
//👇🏻 handles form validation
validate: (values) => {
//👉🏻 validate form inputs
},
});
Let’s re-create the signup form:
import { useFormik } from "formik";
//👇🏻 form validation function
const validate = (values) => {
// validate function
let errors = {};
if (!values.username) {
errors.username = "Required";
} else if (values.username.length < 6) {
errors.username = "Must be 6 characters or more";
}
if (!values.email) {
errors.email = "Required";
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = "Invalid email address";
}
if (!values.password) {
errors.password = "Required";
} else if (values.password.length < 8) {
errors.password = "Must be 8 characters or more";
} else if (values.password === "password") {
errors.password = "Must not be password";
}
return errors;
};
//👇🏻 form submission function
const onSubmit = (values) => {
console.log(values);
alert("Form submitted successfully ✅");
};
const App = () => {
const formik = useFormik({
initialValues: {
username: "",
email: "",
password: "",
},
onSubmit,
validate,
});
return (
<div>
<form onSubmit={formik.handleSubmit}>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<input
type='text'
id='username'
name='username'
{...formik.getFieldProps("username")}
/>
{formik.touched.username ? <p>{formik.errors.username}</p> : null}
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
name='email'
{...formik.getFieldProps("email")}
/>
{formik.touched.email ? (
<p className='text-red-500 mb-4 '>{formik.errors.email}</p>
) : null}
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
name='password'
{...formik.getFieldProps("password")}
/>
{formik.touched.password ? <p>{formik.errors.password}</p> : null}
<button type='submit'>REGISTER</button>
</form>
</div>
)};
From the code snippet above:
Formik allows you to use various validation libraries to help build robust forms with complex validation rules. This enables us to leverage the strengths of both libraries to create scalable and secured forms.
For instance, you can create a validation schema using Yup and add it to the useFormik hook, as shown below:
import { object, string } from "yup";
const App = () => {
//👇🏻 Yup validation schema
const validationSchema = object().shape({
username: string()
.required("Username is required")
.min(6, "Username must be at least 6 characters"),
email: string().email("Invalid email").required("Email is required"),
password: string()
.required("Password is required")
.min(8, "Password must be at least 8 characters"),
});
//👇🏻 useFormik hook using the Yup validation schema
const formik = useFormik({
initialValues: {
username: "",
email: "",
password: "",
},
onSubmit: onSubmit,
validationSchema: validationSchema,
});
};
From the code snippet above, Formik provides a validationSchema property that enables us to use a schema validation library.
It ensures that the values match the defined schema.
Formik provides an easier way of creating forms with the use of components. Formik components enable you to create forms in a cleaner way with less code. They simplify the process of creating dynamic and user-friendly form interfaces within your React applications.
Let’s recreate the signup form using Formik components:
import { Form, Formik, Field, ErrorMessage } from "formik";const App = () => {
const initialValues = {
username: "",
email: "",
password: "",
};
return (
<div>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
<Form>
<h2>Log in</h2>
<label htmlFor='name'>Username</label>
<Field type='text' id='username' name='username' />
<ErrorMessage name='username' render={(msg) => <p>{msg}</p>} />
<label htmlFor='email'>Email</label>
<Field type='email' id='email' name='email' />
<ErrorMessage name='email' render={(msg) => <p>{msg}</p>} />
<label htmlFor='password'>Password</label>
<Field type='password' id='password' name='password' />
<ErrorMessage name='password' render={(msg) => <p>{msg}</p>} />
<button type='submit'>REGISTER</button>
</Form>
</Formik>
</div>
);
};
The Formik component wraps the entire form and accepts the initial values, the `onSubmit` function, and the validation method as props.
Formik also provides other components, such as Form, Field, and ErrorMessage used to replace the form tag, input tag, and error message respectively.
So far, you have learned how to work with forms in React.
We discussed the validation of form inputs using custom JavaScript functions, as well as using Zod and Yup. Additionally, you learned how to use form management libraries, such as React Hook Form and Formik.
Form management libraries, like React Hook and Formik, are suitable for handling form values that require complex validation. Zod and Yup can be used to validate the data received from the form before saving the data to the database.
These tools enable us to build scalable, flexible, and secured forms in React.
Thank you for reading. If you like this blog and want to read more about ReactJS and JavaScript, start reading some recent articles.
Get career, business, writing, and life advice for engineers, tech leads, and engineering managers right to your inbox. Join me at bytesizedbets on growing your career and making smart moves in tech. Simple, direct, and made for you. Subscribe today!
Also published here.