React Hook Form 24 ๐ DevTools โ Debugging Form State in the Browser
React Hook Form’s uncontrolled input model makes debugging different from controlled forms. The inputs hold their own values in the DOM, and React Hook Form tracks state through refs and subscriptions. A console.log of the form state object often prints stale data because the library updates state outside the render cycle. The official DevTools component solves this by subscribing to the form’s internal state and rendering it in a floating panel that updates as the user types, blurs, and submits.
This chapter covers the official @hookform/devtools package: installation, integration with useForm, the panel’s capabilities, and the debugging patterns that replace scattered console.log calls. It also addresses the Chrome extension variant, the limitations of the panel with uncontrolled inputs, and when to reach for the subscribe API instead. The sections that follow explain why form debugging is uniquely difficult, how the DevTools panel connects to the form’s control object, and how to read the state it displays.
Key point: The DevTools panel is a React component that receives the control object from useForm and renders live form state; it is a development-only tool that must not ship to production.
Why form debugging tools exist
The uncontrolled state problem. React Hook Form’s inputs are uncontrolled by design, which is the source of its performance advantage. The consequence is that React DevTools shows the input’s DOM value but not the form’s tracked stateโdirtyFields, touchedFields, errors, and isValid live inside the library’s internal store, not in React state. A developer inspecting a component in React DevTools sees the props and the useForm return object, but that object’s properties do not update in the DevTools panel as the user types .
The stale snapshot problem. Calling getValues() in a render or event handler returns the current value, but calling it in a useEffect or logging formState directly can capture a snapshot that does not reflect the latest interaction. The DevTools panel solves this by re-reading the form state on demand and on a refresh trigger, because React Hook Form’s uncontrolled inputs do not automatically push updates to the panel .
The production exclusion problem. DevTools adds weight and renders a floating UI that must never appear for end users. The package is installed as a dev dependency and the component is typically rendered conditionally or tree-shaken in production builds. A form that imports DevTool unconditionally ships debugging UI to every user .
a. Installing and integrating the DevTools panel
The package is installed as a development dependency and imported only in development code paths.
# Install as a dev dependency, never a runtime dependency
npm install -D @hookform/devtools
The DevTool component requires the control object from useForm. Without it, the panel has no form to inspect and renders nothing. The component is placed anywhere in the component tree, typically after the form element so it does not interfere with layout.
import { useForm } from "react-hook-form";
import { DevTool } from "@hookform/devtools";
export default function SignupForm() {
const { register, control, handleSubmit } = useForm({
mode: "onChange", // validation on change makes the panel update as you type
});
return (
<>
<form onSubmit={handleSubmit((data) => console.log(data))}>
<label htmlFor="email">Email</label>
<input id="email" {...register("email", { required: "Required" })} />
<button type="submit">Submit</button>
</form>
{/* The panel renders bottom-right; pass control, not the whole form object */}
<DevTool control={control} />
</>
);
}
The mode: "onChange" option is not required, but it makes the DevTools panel more responsive. With the default onSubmit mode, validation state changes only after submission, so the panel’s error display remains empty until the user submits .
b. Reading the panel’s state output
The panel displays several categories of state that correspond to formState properties. Understanding what each shows prevents misreading the panel.
// The panel reads from the same internal store as formState
const {
register,
control,
formState: {
errors, // Shown in panel: validation failures per field
dirtyFields, // Shown: fields whose values differ from defaultValues
touchedFields, // Shown: fields that have received and lost focus
isValid, // Shown: true when no errors and constraints pass
isSubmitting, // Shown: true during async onSubmit
},
} = useForm();
The values section shows the current form values read from the uncontrolled inputs. Because the inputs are uncontrolled, the panel provides an Update button that forces a re-read of the latest input values; the panel does not automatically reflect every keystroke unless validation mode triggers a state update . This is the most common source of confusion: typing a value and seeing the panel unchanged does not mean the value is missing from the formโit means the panel has not re-read the DOM.
The errors section shows messages keyed by field name. Clicking a field in the panel’s search can locate the corresponding input in the form, which is useful for large forms with nested field arrays . The valid/invalid indicator mirrors formState.isValid, which depends on the validation mode: in onSubmit mode it stays true until the first submission fails.
c. The Chrome extension variant
A Chrome extension exists that moves the panel into the browser’s DevTools rather than a floating overlay. It requires the same @hookform/devtools package and the same <DevTool control={control} /> component, but renders nothing in the page; the extension detects the component and displays the state inside Chrome DevTools .
// With the extension installed, this component renders nothing in the page
// The extension reads it and displays state in Chrome DevTools
import { DevTool } from "@hookform/devtools";
<DevTool id="signup-form" control={control} />
The id prop is optional; without it, the extension generates a UUID. The benefit of the extension is that it does not cover page content and integrates with the browser’s existing DevTools tabs. The requirement is React Hook Form >= 7.33.1 and @hookform/devtools >= 4.2.2 . The limitation is that it is Chrome-only and requires a separate installation step for every developer on the team.
Complete Example Session
// ============================================
// PART 1: THE FORM WITH DEVTOOLS INTEGRATED
// ============================================
import { useForm } from "react-hook-form";
import { DevTool } from "@hookform/devtools";
type FormValues = {
username: string;
email: string;
age: number;
};
export default function RegistrationForm() {
const {
register,
control,
handleSubmit,
formState: { errors, dirtyFields, isSubmitting },
} = useForm<FormValues>({
mode: "onChange", // makes the DevTools panel update as the user types
defaultValues: {
username: "",
email: "",
age: 18,
},
});
const onSubmit = async (data: FormValues) => {
await new Promise((r) => setTimeout(r, 500));
console.log("Submitted:", data);
};
return (
<>
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
aria-invalid={!!errors.username}
{...register("username", {
required: "Username is required.",
minLength: { value: 3, message: "At least 3 characters." },
})}
/>
{errors.username && <p role="alert">{errors.username.message}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
{...register("email", {
required: "Email is required.",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Enter a valid email.",
},
})}
/>
{errors.email && <p role="alert">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="age">Age</label>
<input
id="age"
type="number"
aria-invalid={!!errors.age}
{...register("age", {
valueAsNumber: true,
min: { value: 18, message: "Must be 18 or older." },
})}
/>
{errors.age && <p role="alert">{errors.age.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submittingโฆ" : "Register"}
</button>
</form>
{/* DevTools panel: bottom-right floating UI */}
<DevTool control={control} />
</>
);
}
// ============================================
// PART 2: WHAT THE PANEL SHOWS AT EACH STAGE
// ============================================
// On initial render:
// values: { username: "", email: "", age: 18 }
// errors: {}
// dirtyFields: {}
// isValid: true (no validation has run in onChange mode until first change)
//
// After typing "ab" in username:
// values: { username: "ab", email: "", age: 18 } (after Update click)
// errors: { username: { message: "At least 3 characters." } }
// dirtyFields: { username: true }
//
// After submitting with valid values:
// isSubmitting: true, then false
// values reflect the submitted payload in the console
// ============================================
// PART 3: CONDITIONAL RENDERING FOR PRODUCTION
// ============================================
import { lazy, Suspense } from "react";
// Lazy-load DevTools only in development
const DevTools =
import.meta.env.DEV
? lazy(() =>
import("@hookform/devtools").then((mod) => ({
default: mod.DevTool,
}))
)
: () => null;
// In the component:
// <Suspense fallback={null}>
// <DevTools control={control} />
// </Suspense>
The parts covered the form with DevTools wired to the control object, the state transitions the panel displays at each interaction stage, and the production exclusion pattern using lazy loading and import.meta.env.DEV.
Quick Reference
DevTools Setup
| Item | Value |
|---|---|
| Package | @hookform/devtools |
| Install command | npm install -D @hookform/devtools |
| Import | import { DevTool } from "@hookform/devtools" |
| Required prop | control from useForm |
| Optional prop | id (for Chrome extension) |
| Render location | Anywhere in the component tree |
Panel Sections
| Section | Shows | Source |
|---|---|---|
| Values | Current input values | DOM inputs (needs Update) |
| Errors | Validation messages | formState.errors |
| Dirty Fields | Changed fields | formState.dirtyFields |
| Touched | Blurred fields | formState.touchedFields |
| Valid | Overall validity | formState.isValid |
| Search | Locate input | Click “Native” button |
Validation Modes and Panel Responsiveness
| Mode | Panel Updates |
|---|---|
onSubmit | Only after submit |
onChange | As user types |
onBlur | When field loses focus |
onTouched | First blur, then change |
Production Exclusion
| Method | How |
|---|---|
| Lazy + DEV flag | import.meta.env.DEV ? lazy(...) : () => null |
| Conditional render | {process.env.NODE_ENV === "development" && <DevTool />} |
| Build exclusion | Tree-shaking with side-effect-free import |
Best Practices
โ Do This:
// Install as dev dependency only
npm install -D @hookform/devtools // โ
// Pass the control object, not the form object
<DevTool control={control} /> // โ
// Use onChange mode for responsive debugging
useForm({ mode: "onChange" }) // โ
// Conditionally render in production
{import.meta.env.DEV && <DevTool control={control} />} // โ
// Click Update in the panel after typing
// (uncontrolled inputs do not push automatically) // โ
// Use the search/Native button for large forms
// to locate inputs in the panel // โ
โ Don’t Do This:
// Install as production dependency
npm install @hookform/devtools // โ
// Forget the control prop
<DevTool /> // โ (renders nothing)
// Ship DevTools unconditionally
<DevTool control={control} /> // in production // โ
// Expect automatic value updates in onSubmit mode
// (panel shows stale values until Update or submit) // โ
// Log formState in useEffect and expect fresh data
useEffect(() => console.log(formState), [formState]); // โ (stale)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Panel renders nothing | control prop missing | Pass control from useForm |
| Values do not update while typing | Uncontrolled inputs + onSubmit mode | Switch to onChange or click Update |
| DevTools visible in production | Import not guarded | Conditional render or lazy load |
| Panel covers form elements | Default position bottom-right | Use placement prop or Chrome extension |
| Cannot find input in large form | Manual search by name | Click “Native” button in panel |
| Extension does not detect form | Version mismatch | RHF >= 7.33.1, devtools >= 4.2.2 |
formState logged outside DevTools is stale | Snapshot captured before update | Use subscribe API or DevTools panel |
Real-World Examples
1. Minimal DevTools integration
const { control } = useForm();
return <DevTool control={control} />;
2. Responsive panel with onChange mode
useForm({ mode: "onChange" });
3. Custom placement
<DevTool control={control} placement="top-left" />
4. Chrome extension with explicit id
<DevTool id="registration-form" control={control} />
5. Production exclusion with NODE_ENV
{process.env.NODE_ENV === "development" && <DevTool control={control} />}
6. Lazy loading for bundle exclusion
const DevTools = lazy(() => import("@hookform/devtools").then(m => ({ default: m.DevTool })));
7. Using subscribe for console debugging without re-renders
useEffect(() => subscribe({
formState: { values: true },
callback: ({ values }) => console.log(values),
}), [subscribe]);
8. Checking dirty state in the panel
// Panel shows dirtyFields after typing; compare with defaultValues
9. Finding a nested field array input
// Type field name in panel search, click Native to scroll to input
10. Verifying submission payload
// After submit, check console.log(data) alongside the panel's values
Visual
The DevTools Data Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HOW THE PANEL READS STATE โ
โ โ
โ <form> โ
โ <input {...register("email")} /> โ
โ โ โ
โ โผ โ
โ React Hook Form internal store โ
โ (errors, dirtyFields, touchedFields, isValid) โ
โ โ โ
โ โ control prop connects the store โ
โ โผ โ
โ <DevTool control={control} /> โ
โ โ โ
โ โผ โ
โ Floating panel renders: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Values โ Errors โ Dirty โ โ
โ โ { ... } โ { ... } โ { ... } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Uncontrolled inputs do not push to the panel; โ
โ Update button re-reads DOM values. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Panel State at Each Interaction
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STATE TRANSITIONS VISIBLE IN PANEL โ
โ โ
โ Initial: โ
โ values: { username: "" } โ
โ errors: {} โ
โ dirtyFields: {} โ
โ isValid: true โ
โ โ โ
โ โผ user types "ab" in username โ
โ values: { username: "ab" } (after Update) โ
โ errors: { username: "At least 3 characters." } โ
โ dirtyFields: { username: true } โ
โ isValid: false โ
โ โ โ
โ โผ user types "abc" โ
โ errors: {} โ
โ isValid: true โ
โ โ โ
โ โผ user clicks Submit โ
โ isSubmitting: true โ false โ
โ onSubmit receives { username: "abc", ... } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Floating Panel vs Chrome Extension
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TWO DEVTOOLS VARIANTS โ
โ โ
โ Floating panel: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ App page โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Form โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโโโโโโโโโโ โ โ
โ โ โ DevTool โ โ โ
โ โ โ panel โ โ โ
โ โ โโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ In-page, covers content, works in any browser โ
โ โ
โ Chrome extension: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ App page (no overlay) โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Form โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Chrome DevTools tab shows form state โ
โ No page overlay, Chrome only โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Package | @hookform/devtools |
| Install | npm install -D @hookform/devtools |
| Component | <DevTool control={control} /> |
| Required prop | control from useForm |
| Panel location | Bottom-right by default |
| Value updates | Manual Update click (uncontrolled inputs) |
| Chrome extension | RHF >= 7.33.1, devtools >= 4.2.2 |
| Production | Conditional render or lazy load |
| Alternative for logs | subscribe API (no re-renders) |
| Panel search | “Native” button locates inputs |
Key takeaways:
- DevTools is a component, not a browser extension by default. It renders a floating panel in your app and requires the
controlobject fromuseFormto connect to the form’s internal store . - Uncontrolled inputs mean manual refresh. The panel does not automatically reflect every keystroke; the Update button re-reads the DOM values, and
onChangevalidation mode makes updates more frequent . - The panel shows what
formStatehides. Errors, dirty fields, touched fields, and validity are visible without scatteringconsole.logcalls or inspecting React DevTools. - The Chrome extension variant moves the panel into DevTools. It requires the same package and component but renders nothing in the page, at the cost of Chrome-only support .
- Never ship DevTools to production. Guard with
import.meta.env.DEV,process.env.NODE_ENV, or lazy loading so the panel and its dependencies are excluded from production bundles. - For programmatic logging, use
subscribe. ThesubscribeAPI provides form state changes outside the render cycle without the panel, useful for autosave drafts or analytics .
Remember: React Hook Form DevTools solves a specific problem: seeing the library’s internal state while the inputs remain uncontrolled. It is not a replacement for React DevTools or browser DevToolsโit is the only window into formState that updates as the form is used. Install it as a dev dependency, pass control, guard the render for production, and use onChange mode to make the panel responsive. When the form misbehaves in a way that console.log cannot capture, the DevTools panel shows the state that explains why.
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!