react-hook-form errors not updating (tested)
react-hook-form (RHF) is a separate library from React's own built-in hooks like useState and useEffect. Its formState object, including formState.errors, is wrapped in a Proxy that only starts tracking a given key once your component reads that key during render. We rendered a real form in jsdom to measure what happens when you read errors correctly, and what the validation output looks like when your component never reads it in render at all.
1. The symptom
You call useForm(), register a required field, submit the form with it empty, and formState.errors is still {} in your component, even though you can see in a callback or in devtools that validation ran and found a problem. The UI never shows the error message.
2. Why: formState is a lazy Proxy
Per the react-hook-form docs, formState is wrapped in a Proxy for performance. RHF does not know which pieces of form state your component actually cares about until you read them. Reading formState.errors inside the component's render body is what tells RHF "subscribe this component to changes in errors." If a key is never read during render, React Hook Form has no reason to re-render that component when the value changes.
This is easy to break by accident: destructuring formState as a whole object and only drilling into .errors inside a submit handler, a useEffect, or behind a condition that is sometimes false on the first render, means the read that "activates" tracking may never happen where RHF can see it.
3. What we measured
We rendered two components with @testing-library/react + jsdom against RHF 7.89.0. Both register a required name field and submit with it empty.
| Component pattern | Where errors is read | Validation ran? | UI showed the error? |
|---|---|---|---|
Reads formState: { errors } and renders errors.name.message directly in JSX | Top-level render body | Yes | Yes — span text was exactly "Name is required" |
Destructures formState as a whole, never touches .errors in the returned JSX; a submit-failure callback separately received the error object | Only inside the handleSubmit error callback, not in render | Yes — the callback logged errs.name.message === "Name is required" | No — the rendered span stayed empty ("") |
Both forms ran the exact same validation and produced the exact same error object. The only difference was whether the component's render body read errors. That one difference was the gap between "validation failed" and "the user sees anything."
4. The fix: read errors where RHF can see it
Read formState.errors (or the specific field, errors.fieldName) directly in your component's top-level render code, not only inside a callback, useEffect, or behind a condition that can be false on first render.
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: "Name is required" })} />
{errors.name && <span>{errors.name.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
In our measured run, this exact pattern (reading errors in the JSX returned by the component) was enough: the error text appeared right after submit, with no extra wiring.
5. If you need errors deep in a tree, use useWatch or useFormState
Per the react-hook-form docs, when a nested or memoized component needs just one field's error without re-rendering the whole form, use the library's own subscription hooks (useFormState({ control, name: "fieldName" }) or field-level Controller) instead of passing the whole errors object down as a prop from a parent that never read it. That guarantees the read happens where RHF expects it, instead of depending on a prop drilled from a render that may skip touching errors under some conditions.
6. Common mistakes that cause this
- Destructuring
const { errors } = formStateinsideonSubmitor auseEffectinstead of in the component body. - Reading
errors.someFieldonly inside anifbranch that is false on first render, so the first render never touches that key. - Passing the entire
formStateobject down as a prop and reading.errorsonly in a child that renders conditionally. - Mixing up react-hook-form's
formStatewith React's ownuseState; they have no relation, and the Proxy behavior described here is specific to react-hook-form.
Frequently asked questions
Why is formState.errors empty even though validation failed?
In our test, this happened when the component never read formState.errors in its render output. Per the react-hook-form docs, formState is a Proxy that only tracks a field once you read it during render; reading it only inside a callback or effect does not register that subscription.
Is this a React bug or a react-hook-form behavior?
It is react-hook-form behavior, not a React bug. react-hook-form is a third-party form library; it is unrelated to React's own built-in hooks like useState and useEffect.
Does destructuring formState break tracking?
Destructuring itself is fine. What matters, per the docs, is whether the specific key (for example errors) is actually read somewhere in the render body on a render path RHF can observe, not just inside a nested callback.
What is the simplest fix?
Read formState.errors (or errors.fieldName) directly in the JSX your component returns, the same render pass that uses register and handleSubmit, instead of only inside onSubmit, useEffect, or a conditional branch.
What if a child component needs just one field's error?
Per the react-hook-form docs, use useFormState({ control, name: 'fieldName' }) or Controller in that child so the read happens where RHF expects it, rather than passing down a formState or errors object read by a parent that never touched that key.
Written with AI assistance. Measured on 2026-10-03 with Node.js 24.21.0, React 18.3.1, react-dom 18.3.1, react-hook-form 7.89.0, @testing-library/react 16.x and jsdom, rendered with React's act(). The general Proxy-based lazy-tracking explanation is per the react-hook-form docs and GitHub issues, not re-derived from source in this test. PlainHeaders is our extension; React, react-hook-form, Testing Library and jsdom are free and open source.