React  

How to Build a Reusable Form Component in React with Custom Hooks?

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?

  • Easy to modify

  • Can create different validation files for different forms

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

  • Reduces repetitive JSX

  • Consistent styling for all inputs

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

  • Prevents duplicate form submissions

  • Shows feedback to the user

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.