Building a Custom Hook for Form Handling in React

person walking on street while holding black umbrella near cars on road at nighttime

Managing forms in React can often lead to repetitive code, especially when handling input validation, state management, and submission logic. Creating a custom hook for form handling can encapsulate this logic, making your code cleaner, more reusable, and easier to maintain. In this post, we’ll walk through building a custom hook for form management.

Why Use a Custom Hook for Forms?

A custom hook allows you to:

  • Encapsulate form logic: Keep form-related logic separate from your components.
  • Reuse across components: Use the same form logic in multiple forms throughout your application.
  • Simplify component code: Reduce the complexity of individual form components by moving logic into the custom hook.

Now, let’s create a custom hook named useForm that manages form state and handles validation.

import { useState } from 'react';

const useForm = (initialValues, validate) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues({
      ...values,
      [name]: value,
    });

    // Validate the field
    if (validate) {
      const fieldErrors = validate(name, value);
      setErrors({
        ...errors,
        ...fieldErrors,
      });
    }
  };

  const handleSubmit = (event, callback) => {
    event.preventDefault();
    if (Object.keys(errors).length === 0) {
      callback(values);
    }
  };

  return {
    values,
    errors,
    handleChange,
    handleSubmit,
  };
};

export default useForm;

So, let’s break down out custom hook and see what’s going on. Looking our code we can see the following:

  1. State Management:
  • The hook starts by initializing two pieces of state: values (which stores form input values) and errors (to store validation errors).
  • initialValues are passed in as a parameter, allowing the hook to be used for various forms with different initial states.

2. handleChange Function:

  • This function updates the form’s values whenever the user types in an input field.
  • It retrieves the input’s name and value from the event object and updates the values state accordingly.
  • After updating the input value, it calls the validate function (if provided) to check for errors and update the errors state.

3. handleSubmit Function:

  • This function is called when the form is submitted.
  • It prevents the default form submission and checks if there are any errors present.
  • If there are no errors, it calls the provided callback function with the current values, allowing the parent component to handle the form data.

📝 Note: Before moving on I’d like to clarify something about the hook. While this implementation is functional, it is essential to note that it might have some issues and this is just for learning purposes.

Validation Logic: The validate function assumes that errors are returned as an object, but this can vary based on how you implement your validation logic. Make sure to adapt it according to your validation strategy (e.g., using a library like Yup).

Error Handling: Currently, the hook only validates inputs on change. You might want to validate all fields on submission as well to catch any remaining errors.

Dynamic Field Support: The hook can be enhanced to support dynamically added fields, such as in cases of multi-step forms or dynamically generated inputs.

Reset Functionality: You could add a method to reset the form to its initial state after submission or for other use cases.

And now, let’s use this custom hook in a functional component.

import React from 'react';
import useForm from './useForm';

const validate = (name, value) => {
  const errors = {};
  if (!value) {
    errors[name] = 'This field is required';
  }
  return errors;
};

const MyForm = () => {
  const { values, errors, handleChange, handleSubmit } = useForm(
    { username: '', email: '' },
    validate
  );

  const onSubmit = (data) => {
    // Here goes the logic to send the data to an API
    console.log('Form Submitted:', data);
  };

  return (
    <form onSubmit={(event) => handleSubmit(event, onSubmit)}>
      <div>
        <label>Username</label>
        <input
          type="text"
          name="username"
          value={values.username}
          onChange={handleChange}
        />
        {errors.username && <p>{errors.username}</p>}
      </div>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
        {errors.email && <p>{errors.email}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

So, the usage is pretty straightforward once we already understand the logic behind our custom hook. But if still unclear, let’s check quickly what’s going on in our implementation code.

  1. Validation Logic:
  • A simple validation function is created that checks if the input value is present. If not, it returns an error message associated with the input’s name.

2. Using the Hook:

  • The useForm hook is called with initial values for username and email, along with the validation function.
  • The returned properties (values, errors, handleChange, and handleSubmit) are destructured for easy access.

3. Rendering the Form:

  • The form consists of two input fields for username and email. Each input field uses the handleChange method to update its value in the state.
  • Error messages are displayed below each input if validation fails.

4. Handling Form Submission:

  • The handleSubmit method prevents the default form submission and validates the data. If no errors are present, it calls onSubmit, where you can process the form data (e.g., sending it to an API).

Conclusion

Creating a custom hook for form handling in React can simplify your codebase and enhance reusability. By encapsulating form state and validation logic, you can create clean, maintainable components that are easier to work with as your application scales. While this implementation provides a solid foundation, consider potential enhancements for flexibility and robustness in real-world applications. My personal recomendation is to go with alternatives like React Hook Forms and Yup that will let you focus only on the actual logic of your form and will save you time from annoying bugs and cover scenarios that might not come up to your head.