{"record":{"id":"e719c9b42103e9da","repo":"spree/spree","slug":"usehostform-must-be-rendered-inside-a-resource-f","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/dashboard-core/src/hooks/use-host-form.ts","lineNumber":20,"sourceCode":"\n/**\n * The react-hook-form instance of the built-in resource form a slot widget is\n * rendered inside (e.g. the product detail form for `product.form_sidebar`).\n * Fields registered against it — `form.register(...)` or `<Controller>` —\n * hydrate, dirty-track, and persist through the host page's own Save button;\n * the widget ships no save logic of its own.\n *\n * Only forms that opt in provide a host form (they wrap themselves in RHF's\n * `FormProvider`). Slots on pages without a page-wide form (orders,\n * customers) have none — calling this there throws. Use\n * `useOptionalHostForm()` for widgets that render in both kinds of context.\n */\nexport function useHostForm<\n  TFieldValues extends FieldValues = FieldValues,\n>(): UseFormReturn<TFieldValues> {\n  const form = useFormContext<TFieldValues>()\n  if (!form) {\n    throw new Error(\n      'useHostForm() must be rendered inside a resource form that exposes its form context ' +\n        '(e.g. a product.form_sidebar slot widget). This page has no host form — ' +\n        'use useOptionalHostForm() and fall back to your own state + API save.',\n    )\n  }\n  return form\n}\n\n/** Like {@link useHostForm}, but returns `null` when there is no host form. */\nexport function useOptionalHostForm<\n  TFieldValues extends FieldValues = FieldValues,\n>(): UseFormReturn<TFieldValues> | null {\n  return useFormContext<TFieldValues>() ?? null\n}\n","sourceCodeStart":2,"sourceCodeEnd":35,"githubUrl":"https://github.com/spree/spree/blob/06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2/packages/dashboard-core/src/hooks/use-host-form.ts#L2-L35","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst form = useHostForm() // throws on order/customer pages\n\n// after\nconst form = useOptionalHostForm()\nif (form) {\n  // host form available: register fields into it\n} else {\n  // no host form: local state + own API save\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"const form = useOptionalHostForm<MyValues>()\nif (form) {\n  // host form present (product.form_sidebar): register into it\n  form.register('my_field')\n} else {\n  // form-less page (orders, customers): own state + API save\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["react","react-hook-form","plugin-api","slots","dashboard-core"],"backgroundTag":"react-hook-form-context-missing","analyzedSha":"06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2","analyzedAt":"2026-08-21T14:59:48.125Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}