spree/spree · error · Error

useHostForm() must be rendered inside a resource form that e

Error message

useHostForm() must be rendered inside a resource form that exposes its form context (e.g. a product.form_sidebar slot widget). This page has no host form — use useOptionalHostForm() and fall back to your own state + API save.

What it means

Slot widgets that save through the host page's form call useHostForm(), which is React Hook Form's useFormContext under the hood. Only opt-in pages wrap their content in RHF's FormProvider (e.g. the product detail form via product.form_sidebar); pages with no page-wide edit form (orders, customers) have no form context, so the hook throws with guidance to use the optional variant and manage state + API save yourself.

Source

Thrown at packages/dashboard-core/src/hooks/use-host-form.ts:20

/**
 * The react-hook-form instance of the built-in resource form a slot widget is
 * rendered inside (e.g. the product detail form for `product.form_sidebar`).
 * Fields registered against it — `form.register(...)` or `<Controller>` —
 * hydrate, dirty-track, and persist through the host page's own Save button;
 * the widget ships no save logic of its own.
 *
 * Only forms that opt in provide a host form (they wrap themselves in RHF's
 * `FormProvider`). Slots on pages without a page-wide form (orders,
 * customers) have none — calling this there throws. Use
 * `useOptionalHostForm()` for widgets that render in both kinds of context.
 */
export function useHostForm<
  TFieldValues extends FieldValues = FieldValues,
>(): UseFormReturn<TFieldValues> {
  const form = useFormContext<TFieldValues>()
  if (!form) {
    throw new Error(
      'useHostForm() must be rendered inside a resource form that exposes its form context ' +
        '(e.g. a product.form_sidebar slot widget). This page has no host form — ' +
        'use useOptionalHostForm() and fall back to your own state + API save.',
    )
  }
  return form
}

/** Like {@link useHostForm}, but returns `null` when there is no host form. */
export function useOptionalHostForm<
  TFieldValues extends FieldValues = FieldValues,
>(): UseFormReturn<TFieldValues> | null {
  return useFormContext<TFieldValues>() ?? null
}

View on GitHub (pinned to 06bf66a868)

Solutions

  1. Switch the widget to useOptionalHostForm() and render a fallback (own useState + API save) when it returns null
  2. Or move the widget to a slot on a form-bearing page such as product.form_sidebar
  3. If the page should host forms, wrap its content in <FormProvider {...form}> the way the product form does so context exists

Example fix

// before
const form = useHostForm() // throws on order/customer pages

// after
const form = useOptionalHostForm()
if (form) {
  // host form available: register fields into it
} else {
  // no host form: local state + own API save
}
Defensive patterns

Strategy: type-guard

Type guard

const form = useOptionalHostForm<MyValues>()
if (form) {
  // host form present (product.form_sidebar): register into it
  form.register('my_field')
} else {
  // form-less page (orders, customers): own state + API save
}

Prevention

When it happens

Trigger: A plugin/widget calling useHostForm() mounted through a slot on a page without a host form (order detail, customer detail), or rendered anywhere outside a FormProvider — e.g. reusing a product form_sidebar widget on an orders page or inside a dialog outside the form tree.

Common situations: Plugin author ships one widget for both form-bearing and form-less pages; a host page gained/lost its form between dashboard versions; copy-pasting a sidebar widget into a standalone dialog.

Related errors


AI-assisted analysis of spree/spree@06bf66a868 (2026-08-21). Data as JSON: /api/errors/e719c9b42103e9da. Report an issue: GitHub.