# Advanced Usage

> Build complex and accessible forms

Source: https://react-hook-form.com/advanced-usage

## Accessibility (A11y)

React Hook Form has support for native form validation, which allows you to 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.

```javascript
import { useForm } from "react-hook-form"

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="name">Name</label>
      <input
        id="name"
        {...register("name", { 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](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA).

```javascript
import { useForm } from "react-hook-form"

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="name">Name</label>

      {/* use aria-invalid to indicate that the field contains an error */}
      <input
        id="name"
        aria-invalid={errors.name ? "true" : "false"}
        {...register("name", { required: true, maxLength: 30 })}
      />

      {/* use role="alert" to announce the error message */}
      {errors.name && errors.name.type === "required" && (
        <span role="alert">This is required</span>
      )}
      {errors.name && errors.name.type === "maxLength" && (
        <span role="alert">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

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](https://github.com/bluebill1049/little-state-machine) as our state management library (you can replace it with [redux](https://github.com/reduxjs/redux) if you are more familiar with it).

**Step 1:** Set up your routes and store.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-wizard-form-9pg6j)

```javascript
import { BrowserRouter, Routes, Route } from "react-router-dom"
import { createStore } from "little-state-machine"
import Step1 from "./Step1"
import Step2 from "./Step2"
import Result from "./Result"

createStore({
  data: {
    firstName: "",
    lastName: "",
  },
})

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Step1 />} />
        <Route path="/step2" element={<Step2 />} />
        <Route path="/result" element={<Result />} />
      </Routes>
    </BrowserRouter>
  )
}
```

**Step 2:** Create your pages, collect and submit the data to the store and push to the next form/page.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-wizard-form-9pg6j)

```javascript
import { useForm } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import { useStateMachine } from "little-state-machine"
import updateAction from "./updateAction"

export default function Step1() {
  const { register, handleSubmit } = useForm()
  const { actions } = useStateMachine({ actions: { updateAction } })
  const navigate = useNavigate()
  const onSubmit = (data) => {
    actions.updateAction(data)
    navigate("/step2")
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="submit" />
    </form>
  )
}
```

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

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-wizard-form-9pg6j)

```javascript
import { useStateMachine } from "little-state-machine"

export default function Result() {
  const { state } = useStateMachine()

  return <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

The idea here is that you can easily compose your form using input components. We are going to create a `Form` component to automatically collect form data.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-smart-form-component-forked-iq89z)

```javascript
import { Form, Input, Select } from "./Components"

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

  return (
    <Form onSubmit={onSubmit}>
      <Input name="firstName" />
      <Input name="lastName" />
      <Select name="gender" options={["female", "male", "other"]} />

      {/* not routed through the custom Input — it has no `name`, so `Form` won't inject `register` for it */}
      <input type="submit" value="Submit" />
    </Form>
  )
}
```

Let's have a look at what is inside each of these components.

`</> Form`

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

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-smart-form-component-forked-iq89z)

```javascript
import { Children, createElement } 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)}>
      {Children.map(children, (child) => {
        return child.props.name
          ? createElement(child.type, {
              ...{
                ...child.props,
                register: methods.register,
                key: child.props.name,
              },
            })
          : child
      })}
    </form>
  )
}
```

`</> Input / Select`

The responsibility of these input components is to register themselves with `react-hook-form`.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-smart-form-component-forked-iq89z)

```javascript
export function Input({ register, name, ...rest }) {
  return <input {...register(name)} {...rest} />
}

export function Select({ register, options, name, ...rest }) {
  return (
    <select {...register(name)} {...rest}>
      {options.map((value) => (
        <option key={value} 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.

---

## Error Messages

Error messages are visual feedback to our users when there are issues with their inputs. React Hook Form provides an `errors` object to let you retrieve errors easily. There are several different ways to improve error presentation on the screen.

- #### Register

  You can simply pass the error message to `register`, via the `message` attribute of the validation rule object, like this:

  `<input {...register('test', { maxLength: { value: 2, message: "error message" } })} />`

- #### Optional Chaining

  The `?.` [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator permits reading the `errors` object without worrying about causing another error due to `null` or `undefined`.

  `errors?.firstName?.message`

- #### Lodash `get`

  If your project is using [lodash](https://lodash.com), then you can leverage the lodash [get](https://lodash.com/docs/4.17.15#get) function. Eg:

  `get(errors, 'firstName.message')`

---

## Connect Form

When we are building forms, there are times when our input lives inside deeply nested component trees; that's when [FormContext](https://react-hook-form.com/docs/useformcontext) comes in handy. However, we can further improve the developer experience by creating a `ConnectForm` component and leveraging React's [renderProps](https://legacy.reactjs.org/docs/render-props.html). The benefit is that you can connect your inputs to React Hook Form much more easily.

```javascript
import { FormProvider, useForm, useFormContext } from "react-hook-form"

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

  return children(methods)
}

export const DeepNest = () => (
  <ConnectForm>
    {({ register }) => <input {...register("deepNestedInput")} />}
  </ConnectForm>
)

export const App = () => {
  const methods = useForm()

  return (
    <FormProvider {...methods}>
      <form>
        <DeepNest />
      </form>
    </FormProvider>
  )
}
```

---

## FormProvider Performance

React Hook Form's [FormProvider](https://react-hook-form.com/docs/formprovider) is built upon [React's Context](https://react.dev/learn/passing-data-deeply-with-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; however, you can still optimize your application if required, as shown in the example below.

**Note:** Using React Hook Form's [Devtools](https://react-hook-form.com/dev-tools) alongside [FormProvider](https://react-hook-form.com/docs/formprovider) can cause performance issues in some situations. Before diving deep in performance optimizations, consider this bottleneck first.

[Open in CodeSandbox](https://codesandbox.io/s/provider-perf-forked-r24ho)

```javascript
import { memo } from "react"
import { useForm, FormProvider, useFormContext } from "react-hook-form"

// we can use memo to prevent re-renders except when the isDirty state changes
const NestedInput = memo(
  ({ register, formState: { isDirty } }) => (
    <div>
      <input {...register("test")} />
      {isDirty && <p>This field is dirty</p>}
    </div>
  ),
  (prevProps, nextProps) =>
    prevProps.formState.isDirty === nextProps.formState.isDirty
)

export const NestedInputContainer = ({ children }) => {
  const methods = useFormContext()

  return <NestedInput {...methods} />
}

export default function App() {
  const methods = useForm()
  const onSubmit = (data) => console.log(data)
  console.log(methods.formState.isDirty) // make sure `formState` is read before rendering to enable the Proxy

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInputContainer />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}
```

---

## Controlled mixed with Uncontrolled Components

React Hook Form embraces uncontrolled components but is also compatible with controlled components. Some components in UI libraries like [MUI](https://github.com/mui/material-ui) and [Antd](https://github.com/ant-design/ant-design) (e.g. `Select`, `Checkbox`) do not expose a native input ref and must be wrapped with `Controller`. Simpler components like `Input` forward the ref and can be used directly with `{...register}`. React Hook Form also optimizes re-rendering for controlled components. Here is an example that combines both patterns with validation.

**Controller**

```javascript
import { Input, Select, MenuItem } from "@mui/material"
import { useForm, Controller } from "react-hook-form"

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

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Controller's own defaultValue is ignored here — useForm's defaultValues takes precedence for the same field */}
      <Controller
        render={({ field }) => (
          <Select {...field}>
            <MenuItem value={10}>Ten</MenuItem>
            <MenuItem value={20}>Twenty</MenuItem>
          </Select>
        )}
        control={control}
        name="select"
      />

      <Input {...register("input")} />

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

**Custom Register**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-controlled-mixed-with-uncontrolled-forked-c323j)

```javascript
import { useEffect } from "react"
import { Input, Select, MenuItem } from "@mui/material"
import { useForm } from "react-hook-form"

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

function App() {
  const { register, handleSubmit, setValue, reset, watch } = useForm({
    defaultValues,
  })
  const selectValue = watch("select")
  const onSubmit = (data) => console.log(data)

  useEffect(() => {
    register("select")
  }, [register])

  const handleChange = (e) => setValue("select", e.target.value)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Select value={selectValue} onChange={handleChange}>
        <MenuItem value={10}>Ten</MenuItem>
        <MenuItem value={20}>Twenty</MenuItem>
      </Select>
      <Input {...register("input")} />

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

---

## Custom Hook with Resolver

You can build a custom hook as a resolver. A custom hook can easily integrate with Yup/Joi/Superstruct as a validation method to be used inside a validation resolver.

- Define a memoized validation schema (or define it outside your component if you don't have any dependencies)
- Use the custom hook, by passing the validation schema
- Pass the validation resolver to the useForm hook

[Open in CodeSandbox](https://codesandbox.io/s/custom-hook-with-resolver-v7-cwczk)

```javascript
import { useCallback } from "react"
import { useForm } from "react-hook-form"
import * as yup from "yup"

const useYupValidationResolver = (validationSchema) =>
  useCallback(
    async (data) => {
      try {
        const values = await validationSchema.validate(data, {
          abortEarly: false,
        })

        return {
          values,
          errors: {},
        }
      } catch (errors) {
        return {
          values: {},
          errors: errors.inner.reduce(
            (allErrors, currentError) => ({
              ...allErrors,
              [currentError.path]: {
                type: currentError.type ?? "validation",
                message: currentError.message,
              },
            }),
            {}
          ),
        }
      }
    },
    [validationSchema]
  )

const validationSchema = yup.object({
  firstName: yup.string().required("Required"),
  lastName: yup.string().required("Required"),
})

export default function App() {
  const resolver = useYupValidationResolver(validationSchema)
  const { handleSubmit, register } = useForm({ resolver })

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="submit" />
    </form>
  )
}
```

---

## Working with virtualized lists

Imagine a scenario where you have a table of data. This table might contain hundreds or thousands of rows, and each row will have inputs. A common practice is to only render the items in the viewport; however, this causes issues as items are removed from the DOM when they are out of view and then re-added. This will cause items to reset to their default values when they re-enter the viewport.

An example is shown below using [react-window](https://github.com/bvaughn/react-window).

**Form**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-with-react-window-forked-3j3mq)

```javascript
import { memo } from "react"
import { FormProvider, useForm, useFormContext } from "react-hook-form"
import { VariableSizeList as List } from "react-window"
import AutoSizer from "react-virtualized-auto-sizer"

const items = Array.from(Array(1000).keys()).map((i) => ({
  title: `List ${i}`,
  quantity: Math.floor(Math.random() * 10),
}))

const WindowedRow = memo(({ index, style, data }) => {
  const { register } = useFormContext()

  return (
    <div style={style}>
      <label>{data[index].title}</label>
      <input {...register(`${index}.quantity`)} />
    </div>
  )
})

export const App = () => {
  const onSubmit = (data) => console.log(data)
  const methods = useForm({ defaultValues: items })

  return (
    <form onSubmit={methods.handleSubmit(onSubmit)}>
      <FormProvider {...methods}>
        <AutoSizer>
          {({ height, width }) => (
            <List
              height={height}
              itemCount={items.length}
              itemSize={() => 100}
              width={width}
              itemData={items}
            >
              {WindowedRow}
            </List>
          )}
        </AutoSizer>
      </FormProvider>
      <button type="submit">Submit</button>
    </form>
  )
}
```

**Field Array**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-usefieldarray-virtual-inputs-ghrey)

```javascript
import { FixedSizeList } from "react-window"
import { Controller, useFieldArray, useForm } from "react-hook-form"

const items = Array.from(Array(1000).keys()).map((i) => ({
  title: `List ${i}`,
  quantity: Math.floor(Math.random() * 10),
}))

function App() {
  const { control, getValues, handleSubmit } = useForm({
    defaultValues: {
      test: items,
    },
  })
  const { fields } = useFieldArray({ control, name: "test" })
  const onSubmit = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FixedSizeList
        width={400}
        height={500}
        itemSize={40}
        itemCount={fields.length}
        itemData={fields}
        itemKey={(i) => fields[i].id}
      >
        {({ style, index, data }) => {
          const defaultValue =
            getValues()["test"][index].quantity ?? data[index].quantity

          return (
            <div style={style}>
              <Controller
                render={({ field }) => <input {...field} />}
                name={`test[${index}].quantity`}
                defaultValue={defaultValue}
                control={control}
              />
            </div>
          )
        }}
      </FixedSizeList>
      <button type="submit">Submit</button>
    </form>
  )
}
```

---

## Testing Form

Testing is important because it prevents bugs and mistakes. It also ensures code safety when refactoring.

We recommend using [testing-library](https://testing-library.com/), because it is simple and tests are more focused on user behavior.

**Step 1:** Set up your testing environment.

Please install [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) with the latest version of `jest`, because react-hook-form uses `MutationObserver` to detect inputs, and to get unmounted from the DOM.

**Note:** If you are using React Native, you don't need to install [@testing-library/jest-dom](https://github.com/testing-library/jest-dom).

```bash
npm install -D @testing-library/jest-dom
```

Create `setup.js` to import [@testing-library/jest-dom](https://github.com/testing-library/jest-dom).

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-docs-066zk?file=/src/setupTests.js)

```javascript
import "@testing-library/jest-dom"
```

**Note:** If you are using React Native, you need to create setup.js, define `window` object, and include the following lines in the setup file:

```javascript
global.window = {}
global.window = global
```

Finally, you have to update `setup.js` in `jest.config.js` to include the file.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-docs-066zk)

```javascript
module.exports = {
  setupFilesAfterEnv: ["<rootDir>/setup.js"], // or .ts for TypeScript App
  // ...other settings
}
```

Additionally, you can set up [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library) and [eslint-plugin-jest-dom](https://github.com/testing-library/eslint-plugin-jest-dom) to follow best practices and anticipate common mistakes when writing your tests.

**Step 2:** Create login form.

We have set the role attribute accordingly. These attributes are helpful when writing tests and improving accessibility. For more information, you can refer to the [testing-library](https://testing-library.com/) documentation.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-docs-066zk?file=/src/App.js)

```javascript
import { useForm } from "react-hook-form"

export default function App({ login }) {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm()
  const onSubmit = async (data) => {
    await login(data.email, data.password)
    reset()
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="email">email</label>
      <input
        id="email"
        {...register("email", {
          required: "required",
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: "Entered value does not match email format",
          },
        })}
        type="email"
      />
      {errors.email && <span role="alert">{errors.email.message}</span>}
      <label htmlFor="password">password</label>
      <input
        id="password"
        {...register("password", {
          required: "required",
          minLength: {
            value: 5,
            message: "min length is 5",
          },
        })}
        type="password"
      />
      {errors.password && <span role="alert">{errors.password.message}</span>}
      <button type="submit">SUBMIT</button>
    </form>
  )
}
```

**Step 3:** Write tests.

The following criteria are what we aim to cover with the tests:

- Test submission failure.

  We are using `waitFor` util and `find*` queries to detect submission feedback, because the `handleSubmit` method is executed asynchronously.

- Test validation associated with each input.

  We are using the `*ByRole` method when querying different elements because that's how users recognize your UI component.

- Test successful submission.

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-docs-066zk?file=/src/App.test.js)

```javascript
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import App from "./App"

const mockLogin = jest.fn((email, password) => {
  return Promise.resolve({ email, password })
})

it("should display required error when value is invalid", async () => {
  render(<App login={mockLogin} />)

  fireEvent.submit(screen.getByRole("button"))

  expect(await screen.findAllByRole("alert")).toHaveLength(2)
  expect(mockLogin).not.toBeCalled()
})

it("should display matching error when email is invalid", async () => {
  render(<App login={mockLogin} />)

  fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
    target: {
      value: "test",
    },
  })

  fireEvent.input(screen.getByLabelText("password"), {
    target: {
      value: "password",
    },
  })

  fireEvent.submit(screen.getByRole("button"))

  expect(await screen.findAllByRole("alert")).toHaveLength(1)
  expect(mockLogin).not.toBeCalled()
  expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue("test")
  expect(screen.getByLabelText("password")).toHaveValue("password")
})

it("should display min length error when password is invalid", async () => {
  render(<App login={mockLogin} />)

  fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
    target: {
      value: "test@mail.com",
    },
  })

  fireEvent.input(screen.getByLabelText("password"), {
    target: {
      value: "pass",
    },
  })

  fireEvent.submit(screen.getByRole("button"))

  expect(await screen.findAllByRole("alert")).toHaveLength(1)
  expect(mockLogin).not.toBeCalled()
  expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue(
    "test@mail.com"
  )
  expect(screen.getByLabelText("password")).toHaveValue("pass")
})

it("should not display error when value is valid", async () => {
  render(<App login={mockLogin} />)

  fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
    target: {
      value: "test@mail.com",
    },
  })

  fireEvent.input(screen.getByLabelText("password"), {
    target: {
      value: "password",
    },
  })

  fireEvent.submit(screen.getByRole("button"))

  await waitFor(() => {
    expect(screen.queryAllByRole("alert")).toHaveLength(0)
    // reset() fires after the async login resolves and triggers a re-render;
    // all post-reset assertions must be inside waitFor so they retry until
    // the DOM reflects the new state.
    expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue("")
    expect(screen.getByLabelText("password")).toHaveValue("")
  })
  expect(mockLogin).toBeCalledWith("test@mail.com", "password")
})
```

#### Resolving act warning during test

If you test a component that uses react-hook-form, you might run into a warning like this, even if you didn't write any asynchronous code for that component:

> Warning: An update to MyComponent inside a test was not wrapped in act(...)

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-act-warning-docs-yq7uj?file=/src/App.js)

```javascript
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit } = useForm({
    mode: "onChange",
  })
  const onSubmit = (data) => {}

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("answer", {
          required: true,
        })}
      />
      <button type="submit">SUBMIT</button>
    </form>
  )
}
```

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-act-warning-docs-yq7uj?file=/src/App.test.js)

```javascript
import { render, screen } from "@testing-library/react"
import App from "./App"

it("should have a submit button", () => {
  render(<App />)

  expect(screen.getByText("SUBMIT")).toBeInTheDocument()
})
```

In this example, there is a simple form without any apparent async code, and the test merely renders the component and checks for the presence of a button. However, it still logs the warning about updates not being wrapped in `act()`.

This is because react-hook-form internally uses asynchronous validation handlers. In order to compute the formState, it has to initially validate the form, which is done asynchronously and results in another render. That update happens after the test function returns, which triggers the warning.

To solve this, wait until some element from your UI appears with `find*` queries. Note that you **must not** wrap your `render()` calls in `act()`. [You can read more about wrapping things in `act` unnecessarily here](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#wrapping-things-in-act-unnecessarily).

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-unit-test-act-warning-docs-tcb7y?file=/src/App.test.js)

```javascript
import { render, screen } from "@testing-library/react"
import App from "./App"

it("should have a submit button", async () => {
  render(<App />)

  expect(await screen.findByText("SUBMIT")).toBeInTheDocument()

  // Now that the UI was awaited until the async behavior was complete,
  // you can keep asserting with `get*` queries.
  expect(screen.getByRole("textbox")).toBeInTheDocument()
})
```

---

## Transform and Parse

Native inputs return values in `string` format unless `valueAsNumber` or `valueAsDate` is used; you can read more in [this section](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement). However, it's not perfect, as we still have to deal with `NaN` or `null` values. Therefore, it's better to handle transformations at the custom hook level. In the following example, we use the `Controller` to handle input and output transformations. You can also achieve a similar result with a custom `register`.

**Important:** spread `field` onto the input before overriding `onChange`/`value`, so `onBlur`, `name`, and `ref` are still forwarded — without `ref`, RHF can't focus the input on a validation error, and without `onBlur`, `isTouched` never updates.

[Open in CodeSandbox](https://codesandbox.io/s/transform-vt3tm)

```javascript
import { Controller } from "react-hook-form"

const ControllerPlus = ({ control, transform, name, defaultValue }) => (
  <Controller
    defaultValue={defaultValue}
    control={control}
    name={name}
    render={({ field }) => (
      <input
        {...field}
        onChange={(e) => field.onChange(transform.output(e))}
        value={transform.input(field.value)}
      />
    )}
  />
)

// usage below:
<ControllerPlus
  transform={{
    input: (value) => (isNaN(value) || value === 0 ? "" : value.toString()),
    output: (e) => {
      const output = parseInt(e.target.value, 10)
      return isNaN(output) ? 0 : output
    },
  }}
  control={control}
  name="number"
  defaultValue=""
/>
```

---

## Server Actions / useActionState

Next.js Server Actions and React's [`useActionState`](https://react.dev/reference/react/useActionState) work well together, but combining them with React Hook Form can look convoluted if you try to sync state between them with `useEffect`. You don't need to: `useActionState` returns a plain dispatch function, so you can call it directly inside `handleSubmit`, and only after client-side validation has passed.

**`actions.js`**: a Server Action that receives the previous state and the validated form data.

```javascript
"use server"

export async function updateProfile(previousState, data) {
  try {
    await db.user.update({ data })
    return { success: true }
  } catch {
    return { success: false, error: "Something went wrong. Please try again." }
  }
}
```

**`profile-form.jsx`**: `handleSubmit` runs validation first, then calls the action dispatcher (`submitAction`) with the already-parsed data, so there is no need to re-parse `FormData` on the server or bridge state with `useEffect`.

```javascript
"use client"

import { useForm } from "react-hook-form"
import { useActionState } from "react"
import { updateProfile } from "./actions"

export default function ProfileForm() {
  const [state, submitAction, isPending] = useActionState(updateProfile, null)
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()

  return (
    <form onSubmit={handleSubmit((data) => submitAction(data))}>
      <input {...register("name", { required: "Name is required" })} />
      {errors.name && <span role="alert">{errors.name.message}</span>}

      <button type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      </button>

      {state?.success && <p>Profile updated.</p>}
      {state?.error && <p role="alert">{state.error}</p>}
    </form>
  )
}
```

`isPending` and `state` come straight from `useActionState` and are read directly in the JSX above, so there's nothing to synchronize: `isPending` replaces a manual loading `useState`, and `state.success` / `state.error` replace a manual result `useState`. The Server Action only runs once `handleSubmit` has confirmed the form is valid.

If the server needs to report field-level errors (for example, "email already in use"), map them onto React Hook Form's error state with [`setError`](https://react-hook-form.com/docs/useform/seterror). This is a legitimate use of `useEffect`, since you are synchronizing an external state source (the action's returned `state`) into the form rather than working around missing render-time state:

```javascript
useEffect(() => {
  if (state?.fieldErrors) {
    for (const [name, message] of Object.entries(state.fieldErrors)) {
      setError(name, { type: "server", message })
    }
  }
}, [state, setError])
```

If you don't need `useActionState`'s pending/result state at all, React Hook Form's own [`Form`](https://react-hook-form.com/docs/useform/form) component can submit directly to a Server Action through its `action` prop, with progressive enhancement handled for you.

---

## Registering Opaque Types

Forms often store richer values than plain strings and numbers — a date picker backed by [Day.js](https://day.js.org/)'s `Dayjs`, a price field backed by [Decimal.js](https://mikemcl.github.io/decimal.js/)'s `Decimal`, or a range backed by Luxon's `DateTime`. React Hook Form's type helpers (`Path`, `DeepPartial`, `FieldErrors`, and others) walk every property of `TFieldValues` to build up field-path inference, and by default that walk recurses into those classes too — their internal properties, getters, and methods included.

For a small form this is harmless. For a large form, or one nested a few levels deep, that extra recursion can be expensive enough to slow down editor type-checking, and in the worst case trip TypeScript's "Type instantiation is excessively deep and possibly infinite" diagnostic.

```tsx
import { useForm } from "react-hook-form"
import type { Dayjs } from "dayjs"

type BookingForm = {
  guest: {
    name: string
    address: {
      line1: string
      line2: string
      city: string
    }
  }
  stay: {
    checkIn: Dayjs
    checkOut: Dayjs
  }
}

// Without registering Dayjs, Path<BookingForm> recurses into every
// property Dayjs exposes (its internal $d, $y, $M, ..., and every
// method) on top of the form's own fields.
const { register } = useForm<BookingForm>()
```

Register `Dayjs` as an opaque type once, in a `.d.ts` file that's part of your program (or any file the module augmentation can reach):

```tsx
import type { Dayjs } from "dayjs"

declare module "react-hook-form" {
  interface OpaqueTypes {
    dayjs: Dayjs
  }
}
```

With that in place, `stay.checkIn` and `stay.checkOut` are treated as single leaf values — the same way `Date` already is — instead of objects to traverse:

```tsx
const { register, control } = useForm<BookingForm>()

// "stay.checkIn" and "stay.checkOut" are valid, fully-inferred field
// paths; TypeScript no longer descends into Dayjs's own properties
// to compute them.
register("stay.checkIn")
register("guest.address.city")
```

The registration is global to your project (declaration merging on `OpaqueTypes` applies wherever `react-hook-form`'s types are used), so it only needs to happen once — typically alongside your other global type declarations — even if `Dayjs`, `Decimal`, or similar rich value types show up in many different forms.

See the [`OpaqueTypes`](https://react-hook-form.com/ts#OpaqueTypes) TypeScript reference for the type definition and further details.
