| | |

React Hook Form 25 ๐Ÿ“‹ Migration Guide โ€” Upgrading from v7 to v8

โฑ๏ธ Estimated read: 11 min | |

React Hook Form v8 is currently in beta, not a stable release. The official migration guide is labeled “BETA,” and the stable line remains v7 (7.85 as of August 2026) . This chapter documents the breaking changes and new patterns for teams evaluating the upgrade, but the first and most important decision is whether to migrate at all in production. The v8 changes are real and substantial: method references become unstable, any becomes unknown, useFieldArray changes its identifier and update semantics, and React Compiler support reshapes how form state must be read.

This chapter covers the installation path, the breaking changes that require code edits, the React Compiler patterns that v8 exists to support, and the migration steps in the order they should be applied. It explains why each change was made and what breaks without it, so the migration is a set of understood decisions rather than a sequence of error-driven fixes. The sections that follow walk through method references, type tightening, field array changes, the removed watch callback, and the setValue restriction.

Key point: v8 is a beta that trades stability for React Compiler compatibility and stricter types; migrate only when the React Compiler is on your roadmap, and never install react-hook-form@beta in a production branch without a rollback plan.


Why v8 migration exists

The React Compiler compatibility problem. React Hook Form’s useForm returns the same object on every render, which is why its API feels stable. The React Compiler cannot tell when a value read through that stable object changes, because the object reference never changes. v8 solves this by giving register, watch, getValues, getErrors, and getFieldState a new reference whenever form state changes, so the compiler detects the dependency and recomputes . This is the single change that cascades into most migration work: any useEffect, useCallback, or useMemo that lists one of these methods as a dependency now re-runs on every form state update.

The any debt problem. v7’s public types used any in callbacks and generic defaults, which let untyped code pass without errors. v8 replaces any with unknown, forcing explicit type annotations. A setValueAs: (v) => v.trim() that compiled in v7 fails in v8 because v is unknown and has no trim method; the fix is (v: string) => v.trim() . The change is deliberate: any hid type errors that caused runtime bugs, and unknown makes them compile-time failures.

The useFieldArray API inconsistency problem. v7’s useFieldArray generated a render identifier called id and spread it into each row, which collided with users’ own id properties from database records. v8 renames the generated identifier to key, removes the keyName prop that existed to work around the collision, and prevents setValue from writing to field arrays so that array updates go through the field array’s own replace method . Each change removes a source of confusion, at the cost of a code edit for every existing field array.

The beta stability trade-off. The official guide opens with npm install react-hook-form@beta, not a version number. The beta has known issues, including a report that formState.errors changes do not cause child components to re-render under the React Compiler in beta.2, a bug later fixed in v7.79 and synced to the beta . Migrating a production form to beta means accepting that the API can change between beta releases before v8.0.0 final.


a. Method references and effect dependencies

The methods that read form state now change identity on every state update. The migration is to stop using them as effect dependencies and to register during render instead.

// BEFORE (v7): register in an effect, with register as a dependency
useEffect(() => {
  register("x", { required: true });
}, [register]); // ran once in v7; re-registers after unregister in v8

// AFTER (v8): register during render
register("x", { required: true });

For effects that need to react to value changes, replace watch with subscribe. The watch callback API is removed entirely in v8; subscribe is its replacement .

// BEFORE (v7): watch with a callback
useEffect(() => {
  const subscription = watch((value) => console.log(value));
  return () => subscription.unsubscribe();
}, [watch]);

// AFTER (v8): subscribe with formValues flag
useEffect(() => {
  const subscription = subscribe({
    formValues: true,
    callback: ({ values }) => console.log(values),
  });
  return () => subscription.unsubscribe();
}, [subscribe]);

b. unknown instead of any

Every callback parameter that was implicitly any now needs an explicit type. The compiler error message names the parameter; annotate it and the error clears.

// BEFORE (v7): v is any, compiles silently
register("name", { setValueAs: (v) => v.trim() });

// AFTER (v8): v is unknown, must be narrowed
register("name", { setValueAs: (v: string) => v.trim() });

The same applies to Controller‘s field.onChange arguments, ChangeHandler event targets, and TContext in resolvers, which now defaults to unknown . In a resolver that uses the default context, narrow before reading, or pass a context type: Resolver<FormValues, MyContext>. For untyped forms, nested dirtyFields and touchedFields are unknown; passing useForm<FormValues>() resolves this.


c. useFieldArray changes

Three edits apply to every field array: field.id becomes field.key, the keyName prop is removed, and setValue on an array is replaced by replace.

// BEFORE (v7): id is the render key, keyName could rename it
const { fields } = useFieldArray({ name: "items", keyName: "customKey" });
fields.map((field) => <input key={field.id} />);

// AFTER (v8): key is the render key, no keyName
const { fields } = useFieldArray({ name: "items" });
fields.map((field) => <input key={field.key} />);
// BEFORE (v7): setValue overwrites the entire array
setValue("items", newItems);

// AFTER (v8): replace from useFieldArray
const { replace } = useFieldArray({ name: "items" });
replace(newItems);

A flat field array is now type-accepted, but the beta guide includes a warning: primitive rows are not safe to use with useFieldArray yet, because fields still spreads each entry internally and drops '', 0, and false. Keep each row as an object in the beta .


Complete Example Session

// ============================================
// PART 1: INSTALL AND VERSION PIN
// ============================================
// npm install react-hook-form@beta
// Verify the installed beta version before migrating:
// npm ls react-hook-form
// Expected: react-hook-form@8.0.0-beta.x

// ============================================
// PART 2: METHOD REFERENCES โ€” REGISTER IN RENDER
// ============================================
import { useForm } from "react-hook-form";

function LegacyRegister() {
  const { register } = useForm();

  // BEFORE: effect with register dependency
  // useEffect(() => { register("x", { required: true }) }, [register]);

  // AFTER: call during render
  register("x", { required: true });

  return <input {...register("x")} />;
}

// ============================================
// PART 3: SUBSCRIBE REPLACES WATCH CALLBACK
// ============================================
import { useEffect } from "react";

function ValueLogger() {
  const { subscribe } = useForm();

  useEffect(() => {
    const subscription = subscribe({
      formValues: true, // subscribe to values, not just formState
      callback: ({ values }) => console.log("values", values),
    });
    return () => subscription.unsubscribe();
  }, [subscribe]);

  return null;
}

// ============================================
// PART 4: TYPED CALLBACKS FOR UNKNOWN
// ============================================
type FormValues = { name: string; age: number };

function TypedForm() {
  const { register } = useForm<FormValues>();

  return (
    <form>
      {/* v7: (v) => v.trim() compiled with any */}
      {/* v8: annotate the parameter */}
      <input
        {...register("name", {
          setValueAs: (v: string) => v.trim(),
        })}
      />
    </form>
  );
}

// ============================================
// PART 5: FIELD ARRAY MIGRATION
// ============================================
type Item = { value: string };

function ItemList() {
  const { control, register } = useForm<{ items: Item[] }>({
    defaultValues: { items: [{ value: "" }] },
  });
  const { fields, append, replace } = useFieldArray({
    control,
    name: "items",
  });

  // BEFORE: fields.map((field) => <input key={field.id} ... />)
  // AFTER: key={field.key}
  return (
    <>
      {fields.map((field, index) => (
        <input
          key={field.key}
          {...register(`items.${index}.value`)}
        />
      ))}
      <button type="button" onClick={() => append({ value: "" })}>
        Add
      </button>
      <button type="button" onClick={() => replace([{ value: "reset" }])}>
        Replace all
      </button>
    </>
  );
}

// ============================================
// PART 6: REACT COMPILER โ€” useFormState IN NESTED COMPONENTS
// ============================================
import { useFormContext, useFormState } from "react-hook-form";

function NestedError() {
  const { register, control } = useFormContext<FormValues>();
  // v8: subscribe with useFormState, not formState from context
  const { errors } = useFormState({ control });

  return (
    <>
      <input {...register("name", { required: "Name is required" })} />
      <p>{errors.name?.message}</p>
    </>
  );
}

The parts covered installation and version verification, the register-in-render migration, the subscribe replacement for the removed watch callback, typed callbacks for unknown, the three useFieldArray edits, and the React Compiler pattern for nested components.


Quick Reference

Breaking Changes Summary

Changev7v8Required Edit
Method referencesStableNew ref on state changeRemove from effect deps
register in effectuseEffect(..., [register])Runs on every updateCall during render
Callback paramsanyunknownAnnotate parameter types
useFieldArray keyfield.idfield.keyRename in key prop
keyName propSupportedRemovedDelete from options
watch callbackSupportedRemovedUse subscribe
setValue on arrayOverwritesNo longer updatesUse replace

React Compiler Patterns

Patternv7v8
Nested error displayformState from contextuseFormState({ control })
Method in effectuseEffect(..., [register])Register in render
Render valueswatchuseWatch
Value changes in effectwatch callbacksubscribe

Migration Order

StepActionRisk
1Pin beta version, run testsBaseline failures
2Fix unknown annotationsCompile errors
3Remove method refs from depsInfinite loops, re-registration
4Replace watch callbackRuntime removal
5Rename field.id to field.keyRender key warnings
6Replace setValue arraysSilent no-op
7Adopt useFormState in nestedStale errors under compiler

Best Practices

โœ… Do This:

// Pin the exact beta version during migration
"react-hook-form": "8.0.0-beta.4"                            // โœ…

// Annotate unknown callback parameters
setValueAs: (v: string) => v.trim()                          // โœ…

// Register during render
register("x", { required: true });                           // โœ…

// Use subscribe for value-change effects
subscribe({ formValues: true, callback: ({ values }) => {} }) // โœ…

// Use field.key as the render key
fields.map((field) => <input key={field.key} />)              // โœ…

// Use replace for array overwrites
const { replace } = useFieldArray({ name: "items" });         // โœ…

// Use useFormState in nested components under the compiler
const { errors } = useFormState({ control });                 // โœ…

โŒ Don’t Do This:

// Unpinned beta in a production branch
npm install react-hook-form@beta                              // โŒ

// Any-typed callback
setValueAs: (v) => v.trim()                                   // โŒ (v is unknown)

// Method as effect dependency
useEffect(() => register("x"), [register]);                   // โŒ (re-runs always)

// Removed watch callback
watch((value) => console.log(value));                         // โŒ

// Old field.id render key
fields.map((field) => <input key={field.id} />)               // โŒ

// setValue on entire array
setValue("items", newItems);                                  // โŒ (no-op in v8)

// formState from context in nested compiled component
const { formState } = useFormContext();                       // โŒ (stale)

Common Pitfalls

PitfallWhy It HappensFix
Infinite re-registrationregister in effect dep listCall register during render
v.trim is not a functionParameter is unknownAnnotate (v: string)
Field array rows not reorderingfield.id no longer existsUse field.key
setValue on array silently failsv8 requires replaceDestructure replace from useFieldArray
Errors stale under compilerformState from context memoizeduseFormState({ control })
watch callback undefinedAPI removedUse subscribe
Beta bug in productionv8 is not stableStay on v7.85 for production

Real-World Examples

1. Register during render

register("x", { required: true });

2. Annotate setValueAs parameter

register("name", { setValueAs: (v: string) => v.trim() });

3. Subscribe for value changes

subscribe({ formValues: true, callback: ({ values }) => log(values) });

4. Field array key rename

fields.map((field) => <input key={field.key} />);

5. Replace array contents

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

6. Remove keyName prop

useFieldArray({ name: "items" }); // no keyName

7. useFormState in nested component

const { errors } = useFormState({ control });

8. Destructure getFieldState under compiler

const { getFieldState } = useForm();
const { isDirty } = getFieldState("name", formState);

9. useWatch to render values

const value = useWatch({ control, name: "email" });

10. Version check before migration

npm ls react-hook-form
# react-hook-form@8.0.0-beta.4

Visual

Breaking Change Dependency Chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHY ONE CHANGE CASCADES                                โ”‚
โ”‚                                                         โ”‚
โ”‚  React Compiler support                                 โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Method refs unstable (register, watch, getValuesโ€ฆ)     โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ”œโ”€โ”€โ–บ useEffect([register]) re-runs              โ”‚
โ”‚         โ”œโ”€โ”€โ–บ useCallback([watch]) re-creates            โ”‚
โ”‚         โ””โ”€โ”€โ–บ useMemo([getValues]) recomputes            โ”‚
โ”‚                                                         โ”‚
โ”‚  any โ†’ unknown                                          โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ”œโ”€โ”€โ–บ setValueAs param must be typed             โ”‚
โ”‚         โ”œโ”€โ”€โ–บ Controller onChange typed                  โ”‚
โ”‚         โ””โ”€โ”€โ–บ resolver TContext defaults unknown         โ”‚
โ”‚                                                         โ”‚
โ”‚  useFieldArray API consistency                          โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ”œโ”€โ”€โ–บ id โ†’ key (render identifier)               โ”‚
โ”‚         โ”œโ”€โ”€โ–บ keyName removed                            โ”‚
โ”‚         โ””โ”€โ”€โ–บ setValue arrays โ†’ replace                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Migration Decision Tree

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SHOULD YOU MIGRATE?                                    โ”‚
โ”‚                                                         โ”‚
โ”‚  Is React Compiler enabled or planned?                  โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”                                          โ”‚
โ”‚    โ–ผ         โ–ผ                                          โ”‚
โ”‚   YES        NO                                         โ”‚
โ”‚    โ”‚         โ”‚                                          โ”‚
โ”‚    โ–ผ         โ–ผ                                          โ”‚
โ”‚  v8 beta    Stay on v7.85                               โ”‚
โ”‚  required   (stable, maintained)                        โ”‚
โ”‚    โ”‚         โ”‚                                          โ”‚
โ”‚    โ–ผ         โ–ผ                                          โ”‚
โ”‚  Accept     No migration                                โ”‚
โ”‚  beta       needed                                     โ”‚
โ”‚  instability โ”‚                                          โ”‚
โ”‚    โ”‚         โ”‚                                          โ”‚
โ”‚    โ–ผ         โ–ผ                                          โ”‚
โ”‚  Run full   Revisit when                               โ”‚
โ”‚  test suite v8.0.0 final                                โ”‚
โ”‚  on branch  ships                                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The useFieldArray Edit Map

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THREE EDITS PER FIELD ARRAY                            โ”‚
โ”‚                                                         โ”‚
โ”‚  BEFORE (v7)                  AFTER (v8)                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ key={field.id}     โ”‚โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ key={field.key}    โ”‚     โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”‚
โ”‚  โ”‚ keyName: "custom"  โ”‚โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ (removed)          โ”‚     โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”‚
โ”‚  โ”‚ setValue("arr", x) โ”‚โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ replace(x)         โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                         โ”‚
โ”‚  Flat arrays:                                           โ”‚
โ”‚  Types accept string[], but rows still spread           โ”‚
โ”‚  internally โ€” keep objects in beta.                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
v8 statusBeta, not stable
Stable versionv7.85 (August 2026)
Install commandnpm install react-hook-form@beta
Method refsregister, watch, getValues, getErrors, getFieldState unstable
Type changeany โ†’ unknown
Field array keyfield.id โ†’ field.key
Array updatesetValue โ†’ replace
Watch callbackRemoved, use subscribe
React CompileruseFormState({ control }) in nested components
Flat field arrayTypes only, not safe in beta

Key takeaways:

  • v8 exists for React Compiler support. The unstable method references are the mechanism that lets the compiler detect form state changes, and they are the source of most migration edits .
  • unknown is a feature, not an obstacle. Every annotation you add is a type error the compiler can now catch, replacing the silent any that let bugs through.
  • useFieldArray has three breaking edits. Rename id to key, delete keyName, and replace setValue array writes with replace .
  • The watch callback is gone. subscribe with formValues: true is the replacement for effects that react to value changes.
  • Production stays on v7. The official guide is labeled beta and the beta has known issues; migrate on a branch, run the full test suite, and wait for v8.0.0 final before merging .
  • Nested components need useFormState. Under the compiler, formState read from useFormContext() stays stale; subscribing with the control object keeps errors live .

Remember: The v7-to-v8 migration is not a version bump; it is a recompile of the assumptions v7 allowed. Method references that never changed now change constantly, any parameters now require types, field arrays use a different identifier, and array writes go through a different method. Each change removes a source of hidden behavior that the React Compiler and stricter TypeScript expose. Do the migration on a branch with the beta pinned, run the existing test suite before and after each category of edit, and keep production on v7 until the beta label is gone.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!