Introduction
Forms are an essential part of most web applications — login, registration, profile update, checkout, and more. But building forms repeatedly in every component can lead to duplicated code, messy logic, and difficult maintenance. A better way is to create reusable form components supported by a custom hook that handles state, validation, input changes, and submission logic. This makes your React code cleaner, reusable, and easier to maintain. In this article, you will learn how to build a reusable form component step-by-step using simple and natural language.
Why Build Reusable Form Components?
Creating reusable form components saves time and improves consistency.
Benefits
Avoid repeating form logic in every component
Centralize validation and state management
Reduce bugs and improve maintainability
Make components cleaner and easier to read
Enable faster development for multiple forms
Designing the useForm Custom Hook
A custom hook lets you extract form logic into a reusable function.
Basic Structure of the Hook
import { useState } from "react";
export function useForm(initialValues, onSubmit) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate(values);
if (Object.keys(validationErrors).length === 0) {
onSubmit(values);
} else {
setErrors(validationErrors);
}
};
return { values, errors, handleChange, handleSubmit };
}
What This Hook Does
Stores form values
Handles changes
Manages validation
Handles form submission
You can now reuse this across multiple form components.
Creating a Separate Validation Function
Validation logic should be separate so it is reusable.
Example Validation
export function validate(values) {
const errors = {};
if (!values.name) {
errors.name = "Name is required";
}
if (!values.email) {
errors.email = "Email is required";
}
return errors;
}
Why Separate Validation?
Building a Reusable Form Component
Now create a form component that uses the custom hook.
Example Form
import { useForm } from "./useForm";
import { validate } from "./validate";
function UserForm() {
const onSubmit = (values) => {
console.log("Form submitted:", values);
};
const { values, errors, handleChange, handleSubmit } = useForm(
{ name: "", email: "" },
onSubmit
);
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
{errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <p className="error">{errors.email}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
export default UserForm;
Why This Is Reusable
Different forms can pass different initial values
Different validation rules can be used
Submission logic is injected through onSubmit
Making Form Fields Reusable with a Custom Input Component
We can go further by creating a reusable input field.
Example
function InputField({ label, name, value, onChange, error }) {
return (
<div>
<label>{label}</label>
<input name={name} value={value} onChange={onChange} />
{error && <span className="error">{error}</span>}
</div>
);
}
Use in Form
<InputField
label="Name"
name="name"
value={values.name}
onChange={handleChange}
error={errors.name}
/>
Benefits
Adding Loading States and Disable Submit Button
Real-world forms need loading state while submitting.
Example Update
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const validationErrors = validate(values);
if (Object.keys(validationErrors).length === 0) {
setLoading(true);
await onSubmit(values);
setLoading(false);
} else {
setErrors(validationErrors);
}
};
User Experience Improvement
Reusing the Form Hook for Multiple Forms
Now you can build many forms using the same logic.
Example Login Form
const { values, errors, handleChange, handleSubmit } = useForm(
{ email: "", password: "" },
loginUser
);
Example Registration Form
const { values, errors, handleChange, handleSubmit } = useForm(
{ name: "", email: "", password: "" },
registerUser
);
Just change the initial values and validation functions.
Best Practices for Building Reusable Forms
Keep validation separate from form logic
Use custom hooks for state and handlers
Create reusable input components
Avoid duplicating state for each field
Use meaningful names: useForm, validate, InputField
Keep forms accessible using proper labels and ARIA tags
Conclusion
Building reusable form components in React with custom hooks helps you avoid repetitive code, improve maintainability, and create cleaner UI structures. By separating form logic, validation, and UI components, you can build multiple forms faster and with fewer bugs. With this pattern, your React applications become more scalable, efficient, and developer-friendly — perfect for both small projects and large-scale applications.