| | |

React Hook Form 14 ๐Ÿ“‹ Form Context โ€” useFormContext and FormProvider for nested components

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

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

APIPurpose
FormProviderPut the useForm methods into the context
useFormContextRead the methods from the context

The context contents

MethodAvailable
registerYes
controlYes
handleSubmitYes
formStateYes
watchYes
setValueYes
resetYes

The context and the controlled components

Componentcontrol prop
ControllerOptional with the context
useControllerOptional with the context

The null return

ContextReturn
Inside the FormProviderThe methods
Outside the FormProvidernull

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

PitfallWhy It HappensFix
The context is nullThe component is outside the providerWrap the component in the FormProvider
The wrong form is readThe nested providers overlapCheck the nearest provider
The useController throwsThe control is not passed and no providerAdd the provider or the control prop
The component is not reusableThe context call is unconditionalAdd 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

ItemValue
The FormProviderPut the useForm methods into the context
The useFormContextRead the methods from the context
The control propOptional with the context
The context valueThe entire useForm return
The null returnOutside the FormProvider
The nested formsThe nearest provider wins

Key takeaways:

  • The FormProvider puts the useForm methods into the React context. The provider receives the methods and the spread is the mechanism that puts them into the context. The useFormContext reads the same object, and the two are the write and the read .
  • The useFormContext removes the prop drilling. The nested components read the register, the control, the formState, and the other methods from the context without the props. The clean API is the reason the context exists .
  • The Controller and the useController read the control from the context. The control prop is optional when the component is inside the FormProvider, 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 useFormContext returns null outside 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.

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!