Skip to content

Advanced

Build complex and accessible forms with React Hook Form.

Accessibility (A11y)

React Hook Form has support for native form validation, which lets you validate inputs with your own rules. Since most of us have to build forms with custom designs and layouts, it is our responsibility to make sure those are accessible (A11y).

The following code example works as intended for validation; however, it can be improved for accessibility.

import React from "react";
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label for="name">Name</label>
      <input type="text" id="name" ref={register({ required: true, maxLength: 30 })} />
      {errors.name && errors.name.type === 'required' && <span>This is required</span>}
      {errors.name && errors.name.type === 'maxLength' && <span>Max length exceeded</span> }
      <input type="submit" />
    </form>
  );
}

The following code example is an improved version by leveraging ARIA.

import React from "react";
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label for="name">Name</label>
      <input
        type="text"
        id="name"
        {/* use aria-invalid to indicate field contain error */}
        aria-invalid={errors.name ? 'true' : 'false'}
        {/* use aria-describedby to associate with error messages */}
        aria-describedby="error-name-required error-name-maxLength"
        ref={register({ required: true, maxLength: 30 })}
      />
      {errors.name && errors.name.type === "required" && (
        {/* the id field is used to associated with aria-describedby*/}
        <span role="alert" id="error-name-required">
          This is required
        </span>
      )}
      {errors.name && errors.name.type === "maxLength" && (
        <span role="alert" id="error-name-maxLength">
          Max length exceeded
        </span>
      )}
      <input type="submit" />
    </form>
  );
}

After this improvement, the screen reader will say: “Name, edit, invalid entry, This is required.”


Wizard Form / Funnel

In this video tutorial, I have demonstrated the core concept of how to build multiple steps funnel with React Hook Form.

It's pretty common to collect user information through different pages and sections. We recommend using a state management library to store user input through different pages or sections. In this example, we are going to use little state machine as our state management library (you can replace it with redux if you are more familiar with it).

Step 1: Set up your routes and store.

CodeSandbox
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { StateMachineProvider, createStore } from "little-state-machine";
import Step1 from "./Step1";
import Step2 from "./Step2";
import Result from "./Result";

import "./styles.css";

createStore({
  data: {}
});

export default function App() {
  return (
    <StateMachineProvider>
      <h1>Page Form Wizard</h1>

      <Router>
        <Route exact path="/" component={Step1} />
        <Route path="/step2" component={Step2} />
        <Route path="/result" component={Result} />
      </Router>
    </StateMachineProvider>
  );
}

Step 2: Create your pages, collect and submit the data to the store and show the next page of your form.

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";
import { withRouter } from "react-router-dom";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";

const Step1 = props => {
  const { register, handleSubmit } = useForm();
  const { action } = useStateMachine(updateAction);
  const onSubmit = data => {
    action(data);
    props.history.push("./step2");
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2>Step 1</h2>
      <label>
        First Name:
        <input name="firstName" ref={register} />
      </label>
      <label>
        Last Name:
        <input name="lastName" ref={register} />
      </label>
      <input type="submit" />
    </form>
  );
};

export default withRouter(Step1);

Step 3: Make your final submission with all the data in the store or display the resulting data.

CodeSandbox
import React from "react";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";

const Step1 = props => {
  const { state } = useStateMachine(updateAction);

  return (
    <>
      <h2>Result:</h2>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </>
  );
};

Following the above pattern, you should be able to build a wizard form/funnel to collect user input data from multiple pages.


Smart Form Component

This idea here is that you can easily compose your form with inputs. We are going to create a Form component to automatically collecting form data.

CodeSandbox
import React from 'react'
import { Form, Input, Select } from './Components';

export default function App() {
  const onSubmit = data => console.log(data);

  return (
    <>
      <h1>Smart Form Component</h1>
      <Form onSubmit={onSubmit}>
        <Input name="firstName" />
        <Input name="lastName" />
        <Select name="sex" options={["female", "male"]} />

        <Input type="submit" value="Submit" />
      </Form>
    </>
  );
}

Let's have a look what's in each of those components.

Form

The Form component's responsibility is to inject all react-hook-form methods into the child component.

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";

export default function Form({ defaultValues, children, onSubmit }) {
  const methods = useForm({ defaultValues });
  const { handleSubmit } = methods;

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {Array.isArray(children)
        ? children.map(child => {
            return child.props.name
              ? React.createElement(child.type, {
                  ...{
                    ...child.props,
                    register: methods.register,
                    key: child.props.name
                  }
                })
              : child;
          })
        : children}
    </form>
  );
}

Input / Select

Those input components' responsibility is to registering them into react-hook-form.

CodeSandbox
import React from "react";

export function Input({ register, name, ...rest }) {
  return <input name={name} ref={register} {...rest} />;
}

export function Select({ register, options, name, ...rest }) {
  return (
    <select name={name} ref={register} {...rest}>
      {options.map(value => (
        <option value={value}>{value}</option>
      ))}
    </select>
  );
}

With the Form component injecting react-hook-form's props into the child component, you can easily create and compose complex forms in your app.


Field Arrays

This is one of the best features about React Hook Form: instead of importing components (like other libraries) to achieve this functionality, you can leverage your existing HTML markup. The key is within the name attribute. In React Hook Form, the name attribute represents the data structure you want to use.

Note: we have also build a custom hook for complex scenario: useFieldArray.

The following example demonstrates how you can create Field Arrays by manipulating the input name attribute.

Note: if your application requires functionality such as: Delete, Insert, Append, Preprend. Here is the link for such implementation. with Controller.

CodeSandbox
import React from "react";
import { useForm, useFieldArray } from "react-hook-form";

function App() {
  const { register, control, handleSubmit } = useForm({
    // defaultValues: {}; you can populate the fields by this attribute 
  });
  const { fields, append, prepend, remove } = useFieldArray({
    control,
    name: "test"
  });
  return (
    <form onSubmit={handleSubmit(data => console.log("data", data))}>
      <ul>
        {fields.map((item, index) => (
          <li key={item.id}>
            {/* for empty validation register make sure pass empty object  */}
            <input name={`test[${index}].name`} ref={register()} /> 
            <button onClick={() => remove(index)}>Delete</button>
          </li>
        ))}
      </ul>
      <section>
        <button type="button" onClick={() => append({ name: "test" })} >
          append
        </button>
        <button type="button" onClick={() => prepend({ name: "test1" })}>
          prepend
        </button>
      </section>
    </form>
  );
}

Schema Validation

React Hook Form supports schema-based form validation with Yup, where you can pass your validationSchema to useForm as an optional config. React Hook Form will validate your input data against the schema and return with either errors or a valid result.

Step 1: Install Yup into your project.

npm install yup

Step 2: Prepare your schema for validation and register inputs with React Hook Form.

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";

const schema = yup.object().shape({
  firstName: yup.string().required(),
  age: yup
    .number()
    .required()
    .positive()
    .integer(),
});

export default function App() {
  const { register, handleSubmit, errors } = useForm({
    validationSchema: schema
  });
  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>
        First Name
        <input type="text" name="firstName" ref={register} />
      </label>
      {errors.firstName && <p>{errors.firstName.message}</p>}
        
      <label>
        Age
        <input type="text" name="age" ref={register} />
      </label>
      {errors.age && <p>{errors.age.message}</p>}
      
      <input type="submit" />
    </form>
  );
}

Error Message

Error messages are visual feedback to our users when there are issues associated with their inputs. In React Hook Form, we provide errors object to let you retrieve errors easily. However, there are several different ways for us to improve render error to the screen.

  • Register

    Embed error message during register, you can easily insert your error message in the message attribute. Eg:

    <input name="test" ref={register({ maxLength: { value: 2, message: "error message" } })} />

  • Optional Chaining

    The optional chaining operator ?. permits reading the errors object without worrying about causing error due to null or undefined.

    errors?.firstName?.message

  • Lodash get

    If your project is using lodash, then you can leverage what lodash get function. Eg:

    get(errors, 'firstName.message')


Connect Form

When we are building forms, there are times when our input lives inside of deeply nested component trees, and that's when FormContext comes in very handy. However, we can further improve the Developer Experience by creating a ConnectForm component and leveraging React's renderProps. The benefit of such a component is you can connect your input with React Hook Form from anywhere.

import { FormContext, useForm, useFormContext } from 'react-hook-form';

export const ConnectForm = ({ children }) => {
 const methods = useFormContext();
 
 return children({
   ...methods
 });
};

// The following example will be the usage
export const DeepNest = () => {
  <ConnectForm>
    {({ register }) => <input ref={register} name="deepNestedInput" />}
  </ConnectForm>
}

export const App = () => {
  const methods = useForm();
  
  return (
    <FormContext {...methods} >
      <form>
       <DeepNest />
      </form>
    </FormContext>
  )
}

FormContext Performance

React Hook Form's FormContext is built upon React's Context API. It solves the problem where data is passed through the component tree without having to pass props down manually at every level. This also causes the component tree to trigger a re-render when React Hook Form triggers a state update, but we can still can optimise our App if required via the example below.


import React from "react";
import { useForm, FormContext, useFormContext } from "react-hook-form";

export default function App() {
  const methods = useForm();
  const onSubmit = data => console.log(data);

  return (
    <FormContext {...methods}>
      // pass all methods into the context
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInput />
        <input type="submit" />
      </form>
    </FormContext>
  );
}

function NestedInput() {
  const {
    register,
    formState: { dirty }
  } = useFormContext();
  
  // we can use React.memo to prevent re-render except dirty state changed
  return React.useMemo(
    () => (
      <div>
        <input name="test" ref={register} />
        {dirty && <p>This field is dirty</p>}
      </div>
    ),
    [dirty]
  );
}


Conditional Controlled Component

In terms of conditional field, React Hook Form made that really simple for us, because the moment when you removed the input form the component tree, it will get unregister automatically, here is an example for such behavior. However, that's not the same case for controlled components since ref is not been registered, and we can do the following:

  • Import Controller to wrap your component and let it manage register and unregister

  • Leverage the use of useEffect with custom register

Here are the examples:

import React, { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@material-ui/core";

function App() {
  const { register, handleSubmit, setValue, watch, control } = useForm();
  const name = watch("name");
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>

      <label>Name:</label>
      <input ref={register} name="name" />

      <label>Conditional Field:</label>
      {name !== "bill" && (
        <Controller
          name="test" 
          as={<TextField />}
          control={control} 
          defaultValue=""
        />
      )}

      <input type="submit" />
    </form>
  );
}

Controlled mixed with Uncontrolled Components

React Hook Form embraces uncontrolled components and also compatible with controlled component. Most of UI library is built to support only controlled components, such as Material-UI and Antd Besides, with React Hook Form controlled component's re-rendering is also been optimized. Here is an example which we combined with controlled and uncontrolled form validation.

import React, { useEffect } from "react";
import { Input, Select, MenuItem } from "@material-ui/core";
import { useForm, Controller } from "react-hook-form";

const defaultValues = {
  select: "",
  input: ""
};

function App() {
  const { handleSubmit, reset, watch, control } = useForm({ defaultValues });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        as={
          <Select>
            <MenuItem value={10}>Ten</MenuItem>
            <MenuItem value={20}>Twenty</MenuItem>
            <MenuItem value={30}>Thirty</MenuItem>
          </Select>
        }
        control={control}
        name="select"
        defaultValue={10}
      />
      
      <Input inputRef={register} name="input" />

      <button type="button" onClick={() => reset({ defaultValues })}>Reset</button>
      <input type="submit" />
    </form>
  );
}

We Need Your Support

If you find React Hook Form to be useful in your React project, please star to support the repo and contributors ❤