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
- Switch the widget to useOptionalHostForm() and render a fallback (own useState + API save) when it returns null
- Or move the widget to a slot on a form-bearing page such as product.form_sidebar
- 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
- Default to useOptionalHostForm() for any widget that may render on both form-bearing and form-less pages
- Document per slot whether its page exposes a host form before publishing a plugin
- Keep an error boundary around slot widgets so one throwing hook doesn't take down the page
- When adding new host pages, wrap content in FormProvider only when a real save flow exists
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
- Create action "${entry.key}" already registered. Use createA
- Create action "${key}" not found.
- A custom field component is already registered for "${namesp
- Form field "${registration.name}" is already registered on t
- Nav entry "${key}" already registered. Use nav.update() inst
AI-assisted analysis of spree/spree@06bf66a868 (2026-08-21).
Data as JSON: /api/errors/e719c9b42103e9da.
Report an issue: GitHub.