Part 112: Crafting a Custom Form Hook in React for Next.js

[App] Server Actions

[App] Server Actions

In our journey through form management in React, we've explored the use of built-in hooks like useFormState and useFormStatus. While these can be helpful, creating a custom hook tailored to your specific needs can often provide a more flexible and maintainable solution. In this blog post, we'll walk through building a custom hook in React to handle form submissions, error states, and loading indicators effectively.

Why Create a Custom Hook?

While React's built-in hooks offer a structured approach to handling form states, they can introduce complexity and limitations, especially when you need custom behavior like resetting a form after submission. A custom hook allows you to encapsulate your logic in a reusable way, making your code cleaner and more adaptable.

Building the useFormState Custom Hook

Let's start by writing a custom hook that manages form submissions, tracks loading states, and handles errors. Here's how we can do it:

Step 1: Define the Hook

First, we'll define our custom hook, useFormState, which will handle form submissions and state management.

// lib/hooks.js
import { useState } from 'react';

export function useFormState(action) {
  const [state, setState] = useState({ loading: false, error: null });
  const handleSubmit = async (event) => {
    event.preventDefault();
    setState({ loading: true, error: null });
    const form = event.currentTarget;
    const formData = new FormData(form);
    const result = await action(formData);
    if (result?.isError) {
      setState({ loading: false, error: result });
    } else {
      form.reset();
      setState({ loading: false, error: null });
    }
  };
  return [state, handleSubmit];
}

One of the advantages of a custom hook is its flexibility. For instance, if you need to perform additional actions after a successful submission, you could extend the hook to accept an onSuccess callback function as an additional parameter.

Explanation:

  • State Management: We use useState to manage two pieces of state: loading and error. The loading state indicates whether the form is in the process of being submitted, and error holds any error message or data that might occur during the submission.

  • handleSubmit Function: This function is responsible for handling the form submission.

    • event.preventDefault(): Stops the default form submission, which would otherwise reload the page.

    • setState({ loading: true, error: null }): Sets loading to true to indicate the start of a submission process and clears previous errors.

    • new FormData(form): Gathers all the form data into a FormData object, which can be easily sent to a server.

    • await action(formData): Executes the action function (passed as an argument to useFormState) with the form data. It waits for the action to complete.

    • If the result indicates an error (result?.isError), the error is stored in the state. Otherwise, the form is reset, and the states are cleared.

  • Return Value: The hook returns an array containing the current state and the handleSubmit function. This pattern is similar to how the useState hook returns a state value and a setter function.

Step 2: Utilize the Hook in a Component

Now that we have our custom hook, we can use it in a component to manage form submissions more cleanly.

// components/CommentForm.jsx
'use client';

import { useFormState } from '@/lib/hooks';
import { createCommentAction } from '@/app/reviews/[slug]/actions';

export default function CommentForm({ slug, title }) {
  const [state, handleSubmit] = useFormState(createCommentAction);

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" placeholder="Name" required />
      <textarea name="comment" placeholder="Comment" required></textarea>
      {state.error && <p>{state.error.message}</p>}
      <button type="submit" disabled={state.loading}>
        {state.loading ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

Explanation:

  • Importing the Hook: The useFormState hook is imported from the hooks.js file.

  • Using the Hook: The hook is invoked with createCommentAction as the action function. This action is responsible for handling the form submission logic specific to creating a comment.

  • Form Elements: The form includes input fields for a name and a comment, both required.

  • Error Handling: If an error exists in the state, it is displayed in a paragraph tag.

  • Submit Button: The button is disabled when the form is loading, and it shows "Submitting..." while the submission is in progress.

Step 3: Test the Functionality

With everything set up, it's time to test our form. Submit an empty form to see if error messages are displayed, and try valid submissions to ensure everything works as expected.

Conclusion

Creating a custom hook like useFormState allows you to encapsulate form logic in a reusable, maintainable, and flexible way. This approach simplifies your component code and provides a tailored solution that can adapt to the specific needs of your application. By handling form submissions, error states, and loading indicators all in one place, you can ensure a consistent and efficient user experience across your application.

Incorporating custom hooks into your React toolkit is a powerful way to enhance your development workflow, making your codebase cleaner and more organized. Explore the possibilities of custom hooks to unlock the full potential of your applications!

Last updated