Skip to content

Advanced

Build complex and accessible forms with React Hook Form.

Accessibility (A11y)

React Hook Form have support on native form validation, which let the borrow validate inputs with your rules, however, as most of us would have to build forms in a custom design and layout and it's our responsibility to make sure our forms 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 } = 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 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-field-name">
          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 the improvement, the screen reader will say: “Name, edit, invalid entry, This is required.”


Wizard Form / Funnel

It's pretty common to collect user information through different pages and sections. We recommend to use state management library to store user input through different pages/section. 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).

Step 1: set up your routes and store.

CodeSandbox
import React from "react";
import ReactDOM from "react-dom";
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 Wizzard</h1>

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

Step 2: create your pages and make them collecting data, submit to store and push to 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 your data in store or display the result 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>
    </>
  );
};

Follow 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 is that you can easily compose your form with inputs. we going to create a Form component to automatically collecting form data. In fact, this is part of what we implemented forms at work.

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

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 App({ 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,
                    ...methods,
                    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 Form component inject react-hook-form's props into the child component, you can easily create and compose forms in your app.


Field Arrays

This is one of cool features about React Hook Form, instead import components like other libraries to achieve the functionality. You can leverage your existing HTML markup. The key is within the name attribute. In React Hook Form, the name attribute represent the data structure.

The following example demonstrate how you can create Field Array by just manipulating the input name attribute.

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

function createArrayWithNumbers(length) {
  return Array.from({ length }, (_, k) => k + 1);
}

export default function App() {
  const { register, handleSubmit } = useForm();
  const [size, setSize] = useState(1);
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {createArrayWithNumbers(size).map(index => {
        return (
          <>
            <label htmlFor="firstName">First Name</label>
            <input
              name={`firstName[${index}]`}
              placeholder="first name"
              ref={register({ required: true })}
            />
            
            <label htmlFor="lastName">Last Name</label>
            <input
              name={`lastName[${index}]`}
              placeholder="last name"
              ref={register({ required: true })}
            />
          </>
        );
      })}

      <button type="button" onClick={() => setSize(size + 1)} >
        Add Person
      </button>
      
      <input type="submit" />
    </form>
  );
}

Schema Validation

React Hook Form supports schema-based form validation with Yup, you can pass your validationSchema to useForm as an optional config, React Hook Form will validate your input data against the schema and return you with errors or 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>
  );
}

Connect Form

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

import { useStateMachine } from 'little-state-machine';
import { useFormContext } from 'react-hook-form';

export const ConnectForm = ({ children }) => {
 const methods = useFormContext();
 
 return props.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 supports schema-based form validation with Yup, you can pass your validationSchema to useForm as an optional config, React Hook Form will validate your input data against the schema and return you with errors or valid result.


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.memo(
    () => (
      <div>
        <input name="test" ref={register} />
        {dirty && <p>This field is dirty</p>}
      </div>
    ),
    [dirty]
  );
}


Need Your Support

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