DEV Community

Chris Westbrook
Chris Westbrook

Posted on • Edited on

Using a custom hook to make form creation a breeze

Writing forms in react can be tedious. There are form libraries that can help, but that means adding another dependency to your project and potentially another layer of complexity that you don't need. Here I will show you how to make a simple set of functions that can be used with any form using a custom hook. I'll assume you know the basics of react, but I will try to explain as much as possible to help beginners. So here goes.

what are hooks?

Some of you may be wondering what hooks are? Perhaps you just started working in react, or you have not dived into the latest and greatest react yet. In short, hooks are an easy way to share stateful logic across components without using crazy things like render props or higher order components. This is made possible because now, the state of your react components can be stored in functions, which can easily be shared between components and even projects. I would highly suggest reading the react hooks documentation for more details.

What should this thing do, anyway?

Think about a form. You might have a signin form with a username and password, or a form that collects order information to be submitted to a database. What parts of the form can be made generic that are common to all form? Well, all forms will need an object to store data, an onChange event to capture changes, an event for validating form input, an onBlur event for keeping track of the touched state of the inputs, and a submit event. It should take as parameters a function for handling specific validation, a function for handling form submission, and a set of initial values for the form. Let’s get started!!

setting up

First let’s start with the basics. We need to import the useState hook from react. Then we need to set up the signature for our hook. I will call it useForm, but you can call it whatever you like. Note that whatever you call it must start with the word use, as there are certain rules with hooks that you can read about in the documentation. Create a new file and paste the code below:

import { useState } from 'react';

const useForm = (handleSubmitCallback, validateCallback, initialValues) => {
Enter fullscreen mode Exit fullscreen mode

There is not a ton going on here, we are just importing the useState hook and then setting up a constant variable equal to an arrow function with the parameters we spoke of above. We will later export this constant from this file so we can use it elsewhere.

setting up state

Now we need to set up the state variables we need. This is done with the useState function which returns an array containing two items, a variable containing the state and a function to set the state value later. These state variables should be pretty self explanatory. I have included them below.

const [form, setForm] = useState(initialValues); //for holding initial form data

    const [errors, setErrors] = useState({}); //for validation errors
    const [success, setSuccess] = useState(false); //set to true if form was submitted successfully
    const [submitting, setSubmitting] = useState(false); //set to true when first submitting the form to disable the submit button
Enter fullscreen mode Exit fullscreen mode
#setting up touch
Enter fullscreen mode Exit fullscreen mode

I mentioned before that we needed to have a variable that kept track of touched status. This is important for displaying form errors. You don’t want to display that a form field is invalid before the user has had a chance to interact with it. The touched variable should initially have the same shape as the form’s initial values, with all fields set to false [not touched]. Below is the code.

 const touchedInitial = {};
        //if the initial values aren't populated than return an empty object.
        if (!form) return {};
        //create a new object using the keys of the form object setting all values to false.
        Object.keys(form).forEach(value => {
            touchedInitial[value] = false;
        });
        return touchedInitial;
    };
    const [touched, setTouched] = useState(setInitialTouched());
Enter fullscreen mode Exit fullscreen mode

setting up validation

Validation is an often overlooked part of form creation. Here I create a form validation wrapper function that calls the function that was passed into the hook, then sets the error state to the value that is returned as well as return that value from itself. The reason the function returns the value as well as setting state is because state changes are not reflected instantly, so if you are going to use a value later in the same function you change the state in, you need to keep a local copy of that value. We will see that in the submit function later. For now here is the validation function.

const validate = () => {
        let e = validateCallback();
        setErrors(e);
        return e;
    };
Enter fullscreen mode Exit fullscreen mode

handleBlur and handleChange

These two events are pretty self-explanatory if you’ve worked with forms in react. I am using object destructuring to get the name and value off the target of the event and then setting state in the form object accordingly.

    const handleChange = e => {
        const { name, value } = e.target; //use destructuring ot get name/value from target for ease of use
        setForm(state => {
            //here we use the spread operator to return the object. This puts the properties of
            //state into a new object and then adds on the newly created value.
            //since properties on the right side of a spread operation always "win", the new value will be returned with the new objecgt.
            return { ...state, [name]: value };
        });
    };
    const handleBlur = e => {
        const { name } = e.target;
        setTouched(c => {
            return { ...c, [name]: true };
        });
        validate();
    };
Enter fullscreen mode Exit fullscreen mode

handling form submission

Honestly this is the part of the hook that I struggled the most with and might need the most improvement. I made handleSubmit an async function because my handleSubmitCallback function that I pass to the hook is expected to return a promise resolving to true or false, indicating a successful form submission or not. I then use this return to set the state of success, which is then returned from the hook so the calling component can do whatever it wishes, i.e. redirect to another component, display a message to the user, etc. Also before form submission happens, all fields are set to touched and the form is validated so all form validation errors will be displayed.

    const handleSubmit = async e => {
        setSubmitting(true);
        //set all fields to touched
        const touchedTrue = {};
        Object.keys(form).forEach(value => {
            touchedTrue[value] = true;
        });
        setTouched(touchedTrue);
        e.preventDefault();
        const err = validate();

        if (Object.keys(err).length === 0) {
            //if there are no errors, set submitting=false and submit form.
            //I am setting submit to false before calling handleSubmitCallback because in my calling component I am performing a redirect with react-router and if I wait until 
            //after I get a warning about trying to set state on an unmounted component.
            setSubmitting(false);
            console.log('no errors.');
            setSuccess(await handleSubmitCallback());
        } else {
            setSubmitting(false);
            setSuccess(false);
        }
    };
Enter fullscreen mode Exit fullscreen mode

wrapping up

Now all that's left to do is return everything from my hook and export it.

return {
        handleChange,
        handleBlur,
        handleSubmit,
        setForm,
        form,
        errors,
        touched,
        submitting,
        success,
    };
};
export default useForm;
Enter fullscreen mode Exit fullscreen mode

Now the calling component simply needs to call the hook with one line of code at the top level of the component:

const { handleChange, handleSubmit, handleBlur, setForm, form, errors, success, submitting } = useForm(
        handleSubmitCallback,
        validationCallback,
        initialValues
    );
Enter fullscreen mode Exit fullscreen mode

Now these functions can be used like so:
<input type="text" name="test" onChange={handleChange}... you get the idea.
You can also use these functions in conjunction with inline onBlur or onChange functions if you needed to run calculations for a specific field like so:

<input onBlur={e=>{
//do calculations here...
handleBlur(e);
}}/>
Enter fullscreen mode Exit fullscreen mode

If you have any suggestions for improvement please feel free to make them. This is my first really big dev.to post, so I would appreciate constructive criticism on how I can improve.
Here is the entire hook source code:

import { useState } from 'react';

const useForm = (handleSubmitCallback, validateCallback, initialValues) => {
    const [form, setForm] = useState(initialValues); //for holding initial form data
    const [errors, setErrors] = useState({}); //for validtion errors
    const [success, setSuccess] = useState(false); //set to true if form was submitted successfully
    const [submitting, setSubmitting] = useState(false); //set to true when first submitting the form to disable the submit button
    //below is a function that creates a touched variable from hte initial values of a form, setting all fields to false (not touched)
    const setInitialTouched = form => {
        const touchedInitial = {};
        //if the initial values aren't populated than return an empty object.
        if (!form) return {};
        //create a new object using the keys of the form object setting alll values to false.
        Object.keys(form).forEach(value => {
            touchedInitial[value] = false;
        });
        return touchedInitial;
    };
    const [touched, setTouched] = useState(setInitialTouched());
    const validate = () => {
        let e = validateCallback();
        setErrors(e);
        return e;
    };
    const handleChange = e => {
        const { name, value } = e.target; //use destructuring ot get name/value from target for ease of use
        setForm(state => {
            //here we use the spread operator to return the object. This puts the properties of
            //state into a new object and then adds on the newly created value.
            //since properties on the right side of a spread operation always "win", the new value will be returned with the new objecgt.
            return { ...state, [name]: value };
        });
    };
    const handleBlur = e => {
        const { name } = e.target;
        setTouched(c => {
            return { ...c, [name]: true };
        });
        validate();
    };
    const handleSubmit = async e => {
        setSubmitting(true);
        //set all fields to touched
        const touchedTrue = {};
        Object.keys(form).forEach(value => {
            touchedTrue[value] = true;
        });
        setTouched(touchedTrue);
        e.preventDefault();
        const err = validate();

        if (Object.keys(err).length === 0) {
            //if there are no errors, set submitting=false and submit form.
            //I am setting submit to false before calling handleSubmitCallback because in my calling component I am performing a redirect with react-router and if I wait until
            //after I get a warning about trying to set state on an unmounted component.
            setSubmitting(false);
            console.log('no errors.');
            setSuccess(await handleSubmitCallback());
        } else {
            setSubmitting(false);
            setSuccess(false);
        }
    };

    return {
        handleChange,
        handleBlur,
        handleSubmit,
        setForm,
        form,
        errors,
        touched,
        submitting,
        success,
    };
};
export default useForm;
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
elizabethschafer profile image
Elizabeth Schafer

Hi - thanks for sharing! This is a great walkthrough, but the formatting is hard to read. A couple of the code blocks look okay, but most of them are just plain text.

Each code block should start and end with three backticks (```) on their own line. You can also add the name of the language after the first three backticks for syntax highlighting to show up (```jsx).

Collapse
 
westbrookc16 profile image
Chris Westbrook

Thank you so much for the feedback!! I wondered if there was something you had to do for code blocks, but I couldn't find it in hte help. I'm kind of new to all this stuff. I have edited the post. I didn't realize it put code in a different font until I looked closer, I am blind so am using nvda and it doesn't announce formatting changes by default unless you set it to. Very good info to know. Thanks again!!

Collapse
 
shekharramola1 profile image
Shekhar Ramola

hey, great piece of code. can you also please add the form template as an example?