DEV Community

Jon Haddow
Jon Haddow

Posted on • Originally published at jon.haddow.me on

Form building with React Hook Form

Within a React application, you may come across a scenario where you want to capture user input. This could be a "Contact Us" form for a blog, a questionnaire, or perhaps an authoring environment for an Event you want to share.

To handle this in React, one approach is to set up a state object, construct inputs, and attach onClick listeners for each field. The form data can be collected from the components state and processed on form submission. This starts off simple, but can lead of complications when handling validation.

This is where a library like React Hook Form comes into play. It relies heavily on uncontrolled inputs which tend to perform better than controlled. It also handles validation well.

React Hook Form has a simple, but powerful API. This article explores that by setting up a form for an Event. We'll cover registering inputs, using Controllers (for custom/third-party inputs) and form validation.


This Event form will include:

  • a title - a plain text input
  • a description- a multiline text area, and
  • a start/end date and time - a 3rd party date picker

First lets setup a new React application (use Create React App to speed up this process), then install react-hook-form and react-datepicker for the date picker.

We'll start by building out the JSX for our form.

export const Form = () => {
    const [startDate, setStartDate] = React.useState(null);
    const [endDate, setEndDate] = React.useState(null);

    return (
        <div className="layout">
            <h1>My Event Form</h1>
            <form>
                <div className="form-section">
                    <label htmlFor="title" className="form-label">
                        Title
                    </label>
                    <input id="title" name="title" type="text" />
                </div>
                <div className="form-section">
                    <label htmlFor="description" className="form-label">
                        Description
                    </label>
                    <textarea id="description" name="description" />
                </div>
                <div className="form-section">
                    <label htmlFor="startDate" className="form-label">
                        Start Date
                    </label>
                    <DatePicker
                        id="startDate"
                        name="startDate"
                        selected={startDate}
                        onChange={(date) => setStartDate(date)}
                        minDate={new Date()}
                        showTimeSelect
                        dateFormat="Pp"
                        selectsStart
                        startDate={startDate}
                        endDate={endDate}
                    />
                </div>
                <div className="form-section">
                    <label htmlFor="endDate" className="form-label">
                        End Date
                    </label>
                    <DatePicker
                        id="endDate"
                        name="endDate"
                        selected={endDate}
                        onChange={(date) => setEndDate(date)}
                        minDate={startDate || new Date()}
                        showTimeSelect
                        dateFormat="Pp"
                        selectsEnd
                        startDate={startDate}
                        endDate={endDate}
                    />
                </div>
                <button type="submit">Submit</button>
            </form>
        </div>
    );
};

Enter fullscreen mode Exit fullscreen mode

Now we'll need to add React Hook Form's useForm hook and deconstruct the handleSubmit and register functions from it.

We'll pass register to each form input ref prop. Let's just cover the title and description for now, and we'll leave the date picker to be handled separately.

We'll setup an onSubmit function to print the data returned from handleSubmit. Here's how our code will look now:

export const Form = () => {
    const [startDate, setStartDate] = React.useState(null);
    const [endDate, setEndDate] = React.useState(null);
    const [submittedData, setSubmittedData] = React.useState({});

    const { handleSubmit, register } = useForm();

    const onSubmit = (data) => {
        setSubmittedData(data);
    };

    return (
        <div className="layout">
            <h1>My Event Form</h1>
            <form onSubmit={handleSubmit(onSubmit)}>
                <div className="form-section">
                    <label htmlFor="title" className="form-label">
                        Title
                    </label>
                    <input id="title" name="title" type="text" ref={register} />
                </div>
                <div className="form-section">
                    <label htmlFor="description" className="form-label">
                        Description
                    </label>
                    <textarea id="description" name="description" ref={register} />
                </div>
                <div className="form-section">
                    <label htmlFor="startDate" className="form-label">
                        Start Date
                    </label>
                    <DatePicker
                        id="startDate"
                        name="startDate"
                        selected={startDate}
                        onChange={(date) => setStartDate(date)}
                        minDate={new Date()}
                        showTimeSelect
                        dateFormat="Pp"
                        selectsStart
                        startDate={startDate}
                        endDate={endDate}
                    />
                </div>
                <div className="form-section">
                    <label htmlFor="endDate" className="form-label">
                        End Date
                    </label>
                    <DatePicker
                        id="endDate"
                        name="endDate"
                        selected={endDate}
                        onChange={(date) => setEndDate(date)}
                        minDate={startDate || new Date()}
                        showTimeSelect
                        dateFormat="Pp"
                        selectsEnd
                        startDate={startDate}
                        endDate={endDate}
                    />
                </div>
                <button type="submit">Submit</button>
            </form>
            <p>Submitted data:</p>
            <pre>{JSON.stringify(submittedData, null, 2)}</pre>
        </div>
    );
};

Enter fullscreen mode Exit fullscreen mode

Give that form a try in the browser. You'll notice the title and description values are printed when the form is submitted, however the start and end dates haven't yet been handled.

Controlled inputs

The 3rd party library used to render these date pickers, aren't using native html form inputs. This means that React Hook Form wouldn't know how to capture the data. These are controlled inputs. To handle them, React Hook Form provides a Controller wrapper component.

Let's try to wrap our start date picker in a Controller:

<Controller
    as={
        <DatePicker
            id="startDate"
            onChange={(date) => setStartDate(date)}
            minDate={new Date()}
            showTimeSelect
            dateFormat="Pp"
            selectsStart
            startDate={startDate}
            endDate={endDate}
        />
    }
    name="startDate"
    control={control}
    valueName="selected"
/>

Enter fullscreen mode Exit fullscreen mode

The key changes that have been made are:

  • the name has been moved up to the Controller. This is so that React Hook Form can track the name of the property and it's value.
  • a control function (which comes from the useForm hook) has been passed into the Controller's control prop.
  • the selected prop on the DatePicker (which was set to the currently selected date/time) has been removed, and the valueName prop on the Controller is set to "selected". This is telling React Hook Form that the name of the property that is anticipating the current form value, is not "value" but rather "selected". In a similar way, if DatePicker had an onEdit method instead of an onChange method, then we'd have to specific that change with the onChangeName prop on the Controller. By default React Hook Form expects the controlled input to have a value prop and a onChange prop. If that's not the case, we need to specify.

These are the main parts needed to hook an external component into our form. Once the end date picker is also wrapped in a Controller, we'll be able to see the data submitted along with the title and description.

Validation

Before the user submits our form, let's add some basic validation checks. Here's our criteria:

  • The title must be provided, and less than 30 characters
  • The description must be less than 100 characters
  • The start date must not be on the 13th 👻 (sorry, just wanted a interesting example...)

React Hook Form provides a simple way to define these rules through the register function. Here's how we'd define the title validation:

<input
    id="title"
    name="title"
    type="text"
    ref={register({
        required: { message: "The title is required", value: true },
        maxLength: {
            message: "The title must be less than 30 characters",
            value: 30,
        },
    })}
/>

Enter fullscreen mode Exit fullscreen mode

When the user submits the form and one of the fields is invalid, the handleSubmit function (on the form onSubmit prop) doesn't trigger the method passed in, but rather updates the errors object that's returned from the useForm hook.

So we want to use this errors object to give visual feedback to the user on what needs to be fixed. Something like this does the job:

<div className="form-section">
    <label htmlFor="title" className="form-label">
        Title
    </label>
    <input
        id="title"
        name="title"
        type="text"
        ref={register({
            required: { message: "The title is required", value: true },
            maxLength: {
                message: "The title must be less than 30 characters",
                value: 30,
            },
        })}
    />
    {errors.title && (
        <span className="error">{errors.title.message}</span>
    )}
</div>

Enter fullscreen mode Exit fullscreen mode

To cover the description, we'd have a similar rule set to the title:

register({
    maxLength: {
        message: "The description must have less than 100 characters",
        value: 100,
    },
})

Enter fullscreen mode Exit fullscreen mode

For the start date, we'll need to use React Hook Form's custom validate function to check that the value isn't on the 13th. We'll need to pass these rules into the Controller's rules prop

<div className="form-section">
    <label htmlFor="startDate" className="form-label">
        Start Date
    </label>
    <Controller
        as={
            <DatePicker
                id="startDate"
                onChange={(date) => setStartDate(date)}
                minDate={new Date()}
                showTimeSelect
                dateFormat="Pp"
                selectsStart
                startDate={startDate}
                endDate={endDate}
            />
        }
        name="startDate"
        control={control}
        valueName="selected"
        rules={{
            validate: (data) => {
                const date = new Date(data);
                return date.getDate() !== 13;
            },
        }}
    />
    {errors.startDate && (
        <span className="error">
            The start date must not be on the 13th!
        </span>
    )}
</div>

Enter fullscreen mode Exit fullscreen mode

You can read more about the rules available in React Hook Form's documentation.


I hope this article gets you more familar with how you can put together a simple form in React. I'll be covering some more tips and tricks with React Hook Form in a future post.

Top comments (0)