React Hook Form 14 ๐ Form Context โ useFormContext and FormProvider for nested components
The FormProvider and the useFormContext are the mechanism that removes the control and the register prop drilling from the deeply nested form components. A form with a section inside a section inside a layout has to pass the control from the form owner to every child that needs it, and the passing is the noise that obscures the component’s actual props. The FormProvider puts the form’s methods into the React context, and the useFormContext reads them from the context without the prop . The component that calls the useFormContext does not know who provided the context, and it does not need to.
The context is the same object that the useForm returns. The FormProvider receives the methods and puts them into the provider, and the useFormContext returns the same methods object. The register, the control, the handleSubmit, the formState, the watch, the setValue, and the reset are all available through the context . The provider is the mechanism that makes the form’s API ambient, and the ambient API is the reason the nested components do not need the props.
The distinction between the FormProvider and the useFormContext is the distinction between the write and the read. The FormProvider is the write: it puts the form into the context. The useFormContext is the read: it takes the form out of the context. The two are the pair, and the pair is the mechanism. The useFormContext outside the FormProvider returns null, and the null is the signal that the component is not inside a form.
Key point: The FormProvider puts the useForm methods into the React context, and the useFormContext reads them from the context. The FormProvider is placed around the <form> element or around the component tree that contains the form. The useFormContext returns the same methods object that the useForm returns, and it returns null outside the provider. The control prop is not needed when the context is used, but the Controller and the useController still require the control from the context.
Why the context exists
The prop-drilling problem. A form with the nested sections has to pass the control from the form owner to every useController and every Controller in the tree. The passing is the noise: the intermediate components do not use the control, and they should not have to accept it. The FormProvider is the mechanism that removes the passing, and the removal is the reason the intermediate components have the clean props.
The multiple-form problem. A page can have the multiple formsโa search form, a filter form, and a settings form. Each form has its own FormProvider, and each provider is scoped to its own subtree. The useFormContext inside the subtree returns the nearest provider’s methods, and the nearest provider is the mechanism that keeps the forms separate . The nesting is the mechanism that lets the components be reused across the forms without the conflict.
The design-system problem. A design system has the TextField that calls the useController, and the useController needs the control. The TextField can accept the control as the prop, or it can read the control from the useFormContext . The context is the mechanism that lets the TextField be used inside the FormProvider without the control prop, and the prop is the mechanism that keeps the dependency explicit. The two are the conventions, and the project chooses.
The partial-context problem. The FormProvider provides the entire useForm return, and the useFormContext returns the entire object. The component can read the register and the formState from the context, and it can call the setValue and the reset without the prop . The entire object is the mechanism that makes the context powerful, and the power is the reason the component does not need the separate props for each method.
The context-default problem. The useFormContext returns null outside the provider. A component that calls the useFormContext and uses the register without the null check throws the runtime error when the component is used outside the form . The null check is the mechanism that keeps the component usable in the both contexts, and the explicit error is the alternative that tells the developer that the component is misused.
a. The FormProvider and the useFormContext basics
The FormProvider wraps the form, and the useFormContext reads the methods in the children. The provider receives the methods object, and the spread is the mechanism that puts the methods into the context.
// The FormProvider and the useFormContext
import { useForm, FormProvider, useFormContext } from "react-hook-form";
type ProfileFormValues = {
name: string;
email: string;
};
function ProfileForm() {
const methods = useForm<ProfileFormValues>({
defaultValues: { name: "", email: "" },
});
return (
// The FormProvider puts the methods into the context.
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => console.log(data))}>
<NameField />
<EmailField />
<button type="submit">Save</button>
</form>
</FormProvider>
);
}
function NameField() {
// The useFormContext reads the methods from the context.
// The component does not need the register or the control prop.
const { register, formState: { errors } } = useFormContext<ProfileFormValues>();
return (
<div>
<input {...register("name")} />
{errors.name && <p role="alert">{errors.name.message}</p>}
</div>
);
}
function EmailField() {
const { register, formState: { errors } } = useFormContext<ProfileFormValues>();
return (
<div>
<input {...register("email")} />
{errors.email && <p role="alert">{errors.email.message}</p>}
</div>
);
}
The useFormContext<ProfileFormValues>() is the mechanism that types the context’s methods. The register and the formState are the methods that the context provides, and the errors.name and the errors.email are the errors that the formState holds . The component does not accept the register or the control prop, and the prop-less API is the reason the nested components are clean.
The provider can be placed at any level of the tree. The common placement is the around the <form> element, and the form element is the boundary that the provider scopes. The FormProvider inside the layout and the useFormContext in the page is the alternative that lets the layout provide the form and the page consume it.
// The provider at the layout level
function FormLayout({ children }: { children: React.ReactNode }) {
const methods = useForm();
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => console.log(data))}>
{children}
</form>
</FormProvider>
);
}
// The page reads the context
function ContactPage() {
return (
<FormLayout>
<NameField />
<EmailField />
</FormLayout>
);
}
The layout provides the form, and the page provides the fields. The separation is the mechanism that lets the layout own the form’s lifecycle and the page own the fields, and the useFormContext is the connection.
b. The context in the Controller and the useController
The Controller and the useController still require the control. The context is the mechanism that provides it without the prop, and the useController reads the control from the context when the prop is omitted.
// The useController with the context
import { useController } from "react-hook-form";
function TextField({ name, label }: { name: string; label: string }) {
// The control is not passed as a prop.
// The useController reads it from the context.
const { field, fieldState } = useController({ name });
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
aria-invalid={fieldState.invalid}
/>
{fieldState.error && <p role="alert">{fieldState.error.message}</p>}
</div>
);
}
The useController({ name }) without the control reads the control from the FormProvider. The control prop is optional when the component is inside the provider, and the omission is the mechanism that keeps the component’s API simple . The Controller component’s control prop is also optional when the component is inside the provider, and the provider is the mechanism that connects the two.
// The Controller with the context
import { Controller } from "react-hook-form";
import { Select } from "@mui/material";
function DepartmentField() {
return (
// The control is not passed as a prop.
// The Controller reads it from the context.
<Controller
name="department"
render={({ field, fieldState }) => (
<Select {...field} error={fieldState.invalid}>
<MenuItem value="engineering">Engineering</MenuItem>
<MenuItem value="design">Design</MenuItem>
</Select>
)}
/>
);
}
The Controller without the control reads it from the context, and the component is used inside the FormProvider without the prop . The two are the same context, and the omission is the mechanism that makes the nested components clean.
The useFormContext returns null outside the provider. The component that reads the context and uses the methods without the null check throws the error, and the check is the mechanism that keeps the component usable in the both contexts.
// The null check
function OptionalField() {
const methods = useFormContext();
// The component can be used outside the form when the check exists.
if (!methods) {
return <input name="optional" />;
}
return <input {...methods.register("optional")} />;
}
The if (!methods) is the mechanism that makes the component usable outside the form. The check is the trade-off against the simpler code, and the choice is the component’s use case.
Complete Example Session
// ============================================
// PART 1: THE FORM PROVIDER
// ============================================
import { useForm, FormProvider } from "react-hook-form";
function ProfileForm() {
const methods = useForm<ProfileFormValues>({
defaultValues: { name: "", email: "" },
});
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => console.log(data))}>
<NameField />
<EmailField />
</form>
</FormProvider>
);
}
// ============================================
// PART 2: THE USE FORM CONTEXT
// ============================================
import { useFormContext } from "react-hook-form";
function NameField() {
const { register, formState: { errors } } = useFormContext<ProfileFormValues>();
return (
<div>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
);
}
// ============================================
// PART 3: THE CONTROLLER WITH THE CONTEXT
// ============================================
import { Controller } from "react-hook-form";
function DepartmentField() {
return (
<Controller
name="department"
render={({ field, fieldState }) => (
<Select {...field} error={fieldState.invalid} />
)}
/>
);
}
// ============================================
// PART 4: THE USE CONTROLLER WITH THE CONTEXT
// ============================================
function TextField({ name, label }: { name: string; label: string }) {
const { field, fieldState } = useController({ name });
return (
<input
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
/>
);
}
// ============================================
// PART 5: THE NESTED FORMS
// ============================================
function Page() {
return (
<FormProvider {...searchMethods}>
<SearchForm />
</FormProvider>
<FormProvider {...filterMethods}>
<FilterForm />
</FormProvider>
);
}
// The nearest provider wins in each subtree.
// ============================================
// PART 6: THE PARTIAL CONTEXT
// ============================================
function PartialField() {
const { register } = useFormContext();
return <input {...register("field")} />;
}
// The component reads only the register from the context.
// ============================================
// PART 7: THE NULL CHECK
// ============================================
function OptionalField() {
const methods = useFormContext();
if (!methods) return <input name="optional" />;
return <input {...methods.register("optional")} />;
}
// The component is usable outside the form.
// ============================================
// PART 8: THE LAYOUT PROVIDER
// ============================================
function FormLayout({ children }: { children: React.ReactNode }) {
const methods = useForm();
return <FormProvider {...methods}>{children}</FormProvider>;
}
// The layout provides the form.
// ============================================
// PART 9: THE FORM STATE
// ============================================
function SubmitButton() {
const { formState: { isSubmitting } } = useFormContext();
return <button disabled={isSubmitting}>Save</button>;
}
// The form state is available through the context.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The FormProvider wraps the form.
// [ ] The useFormContext reads the methods.
// [ ] The useController reads the control from the context.
// [ ] The Controller reads the control from the context.
// [ ] The nested forms have their own providers.
// [ ] The null check is in the component that can be used outside.
The ten parts covered the FormProvider, the useFormContext, the Controller with the context, the useController with the context, the nested forms, the partial context, the null check, the layout provider, the form state, and the checklist. The through-line is that the provider is the write and the context is the read.
Quick Reference
The context pair
| API | Purpose |
|---|---|
FormProvider | Put the useForm methods into the context |
useFormContext | Read the methods from the context |
The context contents
| Method | Available |
|---|---|
register | Yes |
control | Yes |
handleSubmit | Yes |
formState | Yes |
watch | Yes |
setValue | Yes |
reset | Yes |
The context and the controlled components
| Component | control prop |
|---|---|
Controller | Optional with the context |
useController | Optional with the context |
The null return
| Context | Return |
|---|---|
Inside the FormProvider | The methods |
Outside the FormProvider | null |
Best Practices
โ Do This:
// Wrap the form in the FormProvider
<FormProvider {...methods}><form>...</form></FormProvider> // โ
// Read the methods from the context
const { register } = useFormContext(); // โ
// Use the context for the nested components
const { field } = useController({ name: "email" }); // โ
โ Don’t Do This:
// Call the useFormContext outside the provider
const { register } = useFormContext(); // null // โ
// Pass the control prop through the intermediate components
<Section control={control}><Field control={control} /></Section> // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The context is null | The component is outside the provider | Wrap the component in the FormProvider |
| The wrong form is read | The nested providers overlap | Check the nearest provider |
The useController throws | The control is not passed and no provider | Add the provider or the control prop |
| The component is not reusable | The context call is unconditional | Add the null check |
Real-World Examples
1. The FormProvider
<FormProvider {...methods}><form>...</form></FormProvider>
2. The useFormContext
const { register } = useFormContext();
3. The useController with the context
const { field } = useController({ name: "email" });
4. The Controller with the context
<Controller name="email" render={({ field }) => <input {...field} />} />
5. The nested forms
<FormProvider {...searchMethods}><SearchForm /></FormProvider>
<FormProvider {...filterMethods}><FilterForm /></FormProvider>
6. The partial context
const { formState: { errors } } = useFormContext();
7. The null check
const methods = useFormContext();
if (!methods) return null;
8. The layout provider
<FormProvider {...methods}>{children}</FormProvider>
9. The form state
const { formState: { isSubmitting } } = useFormContext();
10. The form reset
const { reset } = useFormContext();
Visual
The Provider and the Context
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE PROVIDER AND THE CONTEXT โ
โ โ
โ FormProvider โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The useForm methods โ The context value โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The nested โ The useFormContext reads it โ
โ โ component โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Nested Forms
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE NESTED FORMS โ
โ โ
โ The page โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The search provider โ The nearest provider wins โ
โ โ โโโ The search form โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The filter provider โ The separate context โ
โ โ โโโ The filter form โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Controller with the Context
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE CONTROLLER WITH THE CONTEXT โ
โ โ
โ The FormProvider โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The control โ The context value โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The Controller โ The control is read from the โ
โ โ (no control prop) โ context โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
The FormProvider | Put the useForm methods into the context |
The useFormContext | Read the methods from the context |
The control prop | Optional with the context |
| The context value | The entire useForm return |
| The null return | Outside the FormProvider |
| The nested forms | The nearest provider wins |
Key takeaways:
- The
FormProviderputs theuseFormmethods into the React context. The provider receives themethodsand the spread is the mechanism that puts them into the context. TheuseFormContextreads the same object, and the two are the write and the read . - The
useFormContextremoves the prop drilling. The nested components read theregister, thecontrol, theformState, and the other methods from the context without the props. The clean API is the reason the context exists . - The
Controllerand theuseControllerread thecontrolfrom the context. Thecontrolprop is optional when the component is inside theFormProvider, and the omission is the mechanism that keeps the components clean . - The nested forms have their own providers. The nearest provider wins in each subtree, and the nesting is the mechanism that keeps the forms separate. The separate forms are the reason the context is scoped, not global .
- The
useFormContextreturnsnulloutside the provider. The component that reads the context and uses the methods without the null check throws the error. The null check is the mechanism that keeps the component usable in the both contexts .
Remember: The React Hook Form context is the FormProvider and the useFormContext pair. The provider puts the useForm methods into the context, and the context reads them without the props. The Controller and the useController read the control from the context, and the nested forms have their own providers. The context is the mechanism that removes the prop drilling, and the null check is the mechanism that keeps the components usable.
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!