Skip to content

Migrate V7 to V8 (BETA)

React Hook Form V8 migration guide.

Installation

npm install react-hook-form@beta

React Compiler

V8 supports the React Compiler. useForm returns the same object on every render, so the compiler can't tell when something read through that object changes. Two patterns keep compiled components up to date:

  • In nested components, subscribe with useFormState instead of reading formState from useFormContext(). The compiler memoizes <FormProvider {...methods}> in the parent, so the context keeps the formState from the first render.
  • Destructure methods such as getFieldState from useForm() instead of calling them on the returned object. The compiler memoizes methods.getFieldState("name") on methods, which never changes.
function NestedInput() {
const { register, control } = useFormContext()
const { errors } = useFormState({ control }) // ✅ updates under the compiler
return (
<>
<input {...register("name", { required: "Name is required" })} />
<p>{errors.name?.message}</p>
</>
)
}

Flat Field Array

V8's types support flat field arrays, allowing simpler data structures when working with useFieldArray.

// Before (V7) — each row had to be an object
useFieldArray({ name: "items" }) // items: { value: string }[]
// After (V8) — primitive values type-check
useFieldArray({ name: "items" }) // items: string[]

Note: as of the current beta, this is a types-only improvement — fields still spreads each entry with { ...field, key } internally, which does not behave as expected for primitive array items. Verify rendering behavior for a primitive items: string[] array before relying on this in production.

Breaking Changes

The following changes require updates to your existing code when upgrading from V7.

Input Ref

register's returned ref is unchanged — it's still a callback ref that receives the real DOM element. This change is about Controller and useController's field.ref instead: in V7, field.ref was a partial proxy object exposing only focus, select, setCustomValidity, and reportValidity. In V8, field.ref is the actual DOM element, so any method or property on the real node is available.

const { field } = useController({ name: "firstName", control })
// Before (V7) — field.ref only exposed a fixed set of methods
field.ref.appendChild // ❌ not available
// After (V8) — field.ref is the actual input element
field.ref.appendChild // ✅ available, like any other DOM node

See #12773 for details.

useFieldArray

Two changes affect useFieldArray:

id renamed to key

The internal render identifier has been renamed from id to key. Update any destructured field references:

const { fields, append } = useFieldArray({ name: "items" })
// fields[0].key is the unique render identifier (was fields[0].id)

Note: you can still append id or key as data — neither affects the render key:

append({
key: "custom-key", // stored as field data, does not override the render key
id: "custom-id", // stored as field data, does not override the render key
})
fields[0].key // unique id used for re-render (auto-generated)

keyName prop removed

The keyName option has been removed from useFieldArray. The render key is always key.

Watch Component

The <Watch /> component prop names has been renamed to name for consistency with the rest of the API.

// Before (V7)
<Watch names={["firstName", "lastName"]} />
// After (V8)
<Watch name={["firstName", "lastName"]} />

watch Callback API

The watch subscription callback overload's exported type (WatchObserver) has been removed from the public API — as of the current beta, the callback still works at runtime, but you lose the named type if you relied on importing it. Use subscribe instead going forward, since it's the supported, fully-typed API for this pattern.

// Before (V7)
watch((value, { name, type }) => console.log(value))
// After (V8) — use subscribe
subscribe({
formState: { values: true },
callback: ({ values }) => console.log(values),
})

setValue

As of the current beta, setValue still directly updates useFieldArray fields, unchanged from V7 — this section describes a planned change that has not yet shipped. If you need to overwrite an entire field array, the replace method from useFieldArray remains the recommended approach in both V7 and V8:

const { replace } = useFieldArray({ name: "items" })
replace(newItems)

See setValue and useFieldArray docs for details.

Thank you for your support

If you find React Hook Form to be useful in your project, please consider starring and supporting it.

Edit