Creating Custom React Hooks: useForm

When working with forms in React, we typically want to have control over the form's state. React makes this easy with the useState hook, but there's still a bit of code to write on our end. Take the following simple example:

function Form() {
    const [formData, setFormData] = React.useState({
        username: '',
        password: '',
    });

    const { username, password } = formData;

    const handleInputChange = (e) => {
        setFormData({ ...form, [e.target.name]: e.target.value });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.dir(formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                name="username"
                value={username}
                onChange={handleInputChange}
            />
            <input
                type="password"
                name="password"
                value={password}
                onChange={handleInputChange}
            />
            <button type="submit">Submit</button>
        </form>
    );
}

For one form, this is straightforward and not too taxing on our part. But what if we have lots of forms like this on our site? Re-writing the state management multiple times seems like more work than necessary for us and would probably introduce a lot of mistakes.

Instead, let's convert the state management to a custom hook that we'll call useForm.

Let's start by managing our form state in useForm. The user should be able to pass in the initial state as a parameter:

const useForm = (initialState = {}) => {
    const [formData, setFormData] = React.useState(initialState);

    return { formData };
};

It would also be nice to not have to re-write handleInputChange either, so let's add that to the hook:

const useForm = (initialState = {}) => {
    const [formData, setFormData] = React.useState(initialState);

    const handleInputChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    return { formData, handleInputChange };
};

Great! Now we can just get handleInputChange from useForm and pass that to our inputs' onChange.

This is what our previous example looks like now with useForm:

function Form() {
    const { formData, handleInputChange } = useForm({
        username: '',
        password: '',
    });

    const { username, password } = formData;

    const handleSubmit = (e) => {
        e.preventDefault();
        console.dir(formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                name="username"
                value={username}
                onChange={handleInputChange}
            />
            <input
                type="password"
                name="password"
                value={password}
                onChange={handleInputChange}
            />
            <button type="submit">Submit</button>
        </form>
    );
}

Finally, let's return a handleSubmit function from useForm so that we can reuse that logic in our forms' onSubmit. We'll want to call e.preventDefault() to prevent the page from reloading, but it would also be nice if the user could add some custom behavior when the submit handler is called.

Let's add another parameter to useForm: an onSubmit function that takes in the formData. useForm's handleSubmit can take care of preventing the default behavior, then call the user's onSubmit function and pass it the formData.

const useForm = (initialState = {}, onSubmit) => {
    const [formData, setFormData] = React.useState(initialState);

    const handleInputChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        onSubmit?.(formData);
    };

    return { formData, handleInputChange, handleSubmit };
};

Here's the final result with our custom onSubmit function passed to useForm:

function Form() {
    const { formData, handleInputChange, handleSubmit } = useForm(
        {
            username: '',
            password: '',
        },
        (formData) => console.dir(formData)
    );

    const { username, password } = formData;

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                name="username"
                value={username}
                onChange={handleInputChange}
            />
            <input
                type="password"
                name="password"
                value={password}
                onChange={handleInputChange}
            />
            <button type="submit">Submit</button>
        </form>
    );
}

That's it! Thanks to React hooks, we can create nice reusable form data logic that can be used across our app's forms.

Let's connect

Come connect with me on LinkedIn, Twitter, and GitHub!

If you found this post helpful, please consider supporting my work financially:

☕️Buy me a coffee!

Subscribe for the latest news

Sign up for my mailing list to get the latest blog posts and content from me. Unsubscribe anytime.