/react-hook-form
Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
$ npx -y skills add supabase/supabase --skill react-hook-form --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/react-hook-form
Context preview
The summary Claude sees to decide when to auto-load this skill.
Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
SKILL.md
react-hook-form.SKILL.mdname: react-hook-form
description: Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
reset, dirty state, number inputs, and controlled-input rules. Load this BEFORE
writing or modifying ANY form code, adding a field to an existing form, touching
watch/useWatch/formState/getValues/setValue/reset, wiring a form into a dialog or
sheet, or building a submit/cancel footer — even when the change looks trivial.
The codebase contains widespread RHF anti-patterns; without this skill you will
copy them. For form layout and which components to use, also load
studio-ui-patterns.
React Hook Form
How to write forms that stay correct as they grow. The existing codebase is **not** a safe reference: `form.watch()` off prop-drilled form objects, subscription-only watches, unguarded `valueAsNumber`, and `?? undefined` controlled values are all common in older code and all wrong. Follow this skill, not the neighboring file.
**Policy — fix what you touch.** New code must follow these rules. When you modify existing form code, upgrade the specific fields/hooks/components you're editing to match (e.g. a component you touch that calls `form.watch` gets converted to `useWatch`). Leave untouched code alone, but tell the user about anti-patterns you noticed and didn't fix. Never add new violations: `react-hook-form/no-use-watch` is ratcheted in Studio CI — any increase in the warning count fails the build.
Mental model: subscriptions decide who re-renders
RHF is uncontrolled at heart. Values live in refs; nothing re-renders unless a subscription says so. Every read API is a subscription decision:
| API | Subscribes | Re-renders | Use for | | ----------------------------- | ---------- | -------------------------- | ---------------------------------------------- | | `useWatch({ control, name })` | yes | only the calling component | reactive value reads, anywhere | | `useFormState({ control })` | yes | only the calling component | `isDirty`/`errors`/etc. outside the form owner | | `formState` (destructured) | yes | the `useForm` owner | form state **in the owner component only** | | `form.watch(name)` | yes | the **entire form tree** | avoid — lint-flagged, see below | | `getValues()` | no | never | event handlers and `onSubmit` only | | `subscribe()` | callback | none | side effects outside render |
Two facts explain most of the bugs we've shipped:
1. **`form.watch()` and `form.formState` hoist their subscription to the `useForm` owner**, no matter which component calls them. A child that reads `form.watch('x')` off a prop works today only because the whole tree re-renders on every change — it silently goes stale the moment anyone adds `React.memo` between owner and child, and until then it re-renders every sibling on every keystroke. A no-arg `form.watch()` sets `watchAll` and re-renders the tree on every field change for the life of the form. 2. **`formState` is a Proxy** — reading a property is what arms the subscription. Destructure it (`const { isDirty } = form.formState`), never pass the object around or read it conditionally (`a && formState.isValid` may never subscribe). Enforced by `react-hook-form/destructuring-formstate` (error).
Reading values, by location
- **In the component that owns `useForm`:** destructure `formState`; prefer
`useWatch` over `form.watch` even here (the `no-use-watch` rule flags every `watch`, and `useWatch` scopes the re-render if the JSX is later extracted).
- **In any child component or custom hook:** accept `control` (not the whole
`form`) and use `useWatch({ control, name })` / `useFormState({ control })`. Inside `<Form {...form}>` (which _is_ `FormProvider`), `useFormContext()` + `useWatch({ name })` also works and avoids prop-drilling entirely.
- **Consume the return value.** Never call a watch for its subscription side
effect and then read via `getValues()` — the watch list and the read list will drift apart (it has already happened; fields silently lost reactivity). The value you render must _be_ the value you subscribed to.
- **One read path per value per render.** Mixing `useWatch('x')` on one line and
`getValues('x')` a few lines later lets the two disagree within a single render.
- **Name what you watch.** `useWatch({ control })` with no `name` re-renders on
every keystroke in every field. Subscribe to the specific names you use.
- `watch(callback)` is deprecated — use `subscribe()` for render-free listeners,
and always return its cleanup from `useEffect`.
// ❌ common in the codebase — all three subscriptions hoist to the form owner
function Fields({ form }: { form: UseFormReturn<FormValues> }) {
form.watch(['storageType', 'totalSize']) // return value discarded
const { errors } = form.formState // prop-form formState
const size = form.getValues('totalSize') // non-reactive read in render
...
}
// ✅ child subscribes for itself and consumes what it watches
function Fields({ control }: { control: Control<FormValues> }) {
const [storageType, totalSize] = useWatch({ control, name: ['storageType', 'totalSize'] })
const { errors } = useFormState({ control })
...
}The canonical form
zod schema → `z.infer` type → `useForm` with `zodResolver` and **complete** `defaultValues` → `<Form {...form}>` → `FormField` render-prop per field → `FormItemLayout` → `FormControl` → primitive from `ui`. Layout/container choices (Card vs Sheet, `layout=` variants) are covered by the `studio-ui-patterns` skill and the demos in `apps/design-system/registry/default/example/` (`form-patterns-pagelayout.tsx`, `form-patterns-sidepanel.tsx`) —
Read more
name: react-hook-form description: Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions, reset, dirty state, number inputs, and controlled-input rules. Load this BEFORE writing or modifying ANY form code, adding a field to an existing form, touching watch/useWatch/formState/getValues/setValue/reset, wiring a form into a dialog or sheet, or building a submit/cancel footer — even when the change looks trivial. The codebase contains widespread RHF anti-patterns; without this skill you will copy them. For form layout and which components to use, also load studio-ui-patterns.
React Hook Form
How to write forms that stay correct as they grow. The existing codebase is **not** a safe reference: `form.watch()` off prop-drilled form objects, subscription-only watches, unguarded `valueAsNumber`, and `?? undefined` controlled values are all common in older code and all wrong. Follow this skill, not the neighboring file.
**Policy — fix what you touch.** New code must follow these rules. When you modify existing form code, upgrade the specific fields/hooks/components you're editing to match (e.g. a component you touch that calls `form.watch` gets converted to `useWatch`). Leave untouched code alone, but tell the user about anti-patterns you noticed and didn't fix. Never add new violations: `react-hook-form/no-use-watch` is ratcheted in Studio CI — any increase in the warning count fails the build.
Mental model: subscriptions decide who re-renders
RHF is uncontrolled at heart. Values live in refs; nothing re-renders unless a subscription says so. Every read API is a subscription decision:
| API | Subscribes | Re-renders | Use for | | ----------------------------- | ---------- | -------------------------- | ---------------------------------------------- | | `useWatch({ control, name })` | yes | only the calling component | reactive value reads, anywhere | | `useFormState({ control })` | yes | only the calling component | `isDirty`/`errors`/etc. outside the form owner | | `formState` (destructured) | yes | the `useForm` owner | form state **in the owner component only** | | `form.watch(name)` | yes | the **entire form tree** | avoid — lint-flagged, see below | | `getValues()` | no | never | event handlers and `onSubmit` only | | `subscribe()` | callback | none | side effects outside render |
Two facts explain most of the bugs we've shipped:
1. **`form.watch()` and `form.formState` hoist their subscription to the `useForm` owner**, no matter which component calls them. A child that reads `form.watch('x')` off a prop works today only because the whole tree re-renders on every change — it silently goes stale the moment anyone adds `React.memo` between owner and child, and until then it re-renders every sibling on every keystroke. A no-arg `form.watch()` sets `watchAll` and re-renders the tree on every field change for the life of the form. 2. **`formState` is a Proxy** — reading a property is what arms the subscription. Destructure it (`const { isDirty } = form.formState`), never pass the object around or read it conditionally (`a && formState.isValid` may never subscribe). Enforced by `react-hook-form/destructuring-formstate` (error).
Reading values, by location
- **In the component that owns `useForm`:** destructure `formState`; prefer
`useWatch` over `form.watch` even here (the `no-use-watch` rule flags every `watch`, and `useWatch` scopes the re-render if the JSX is later extracted).
- **In any child component or custom hook:** accept `control` (not the whole
`form`) and use `useWatch({ control, name })` / `useFormState({ control })`. Inside `<Form {...form}>` (which _is_ `FormProvider`), `useFormContext()` + `useWatch({ name })` also works and avoids prop-drilling entirely.
- **Consume the return value.** Never call a watch for its subscription side
effect and then read via `getValues()` — the watch list and the read list will drift apart (it has already happened; fields silently lost reactivity). The value you render must _be_ the value you subscribed to.
- **One read path per value per render.** Mixing `useWatch('x')` on one line and
`getValues('x')` a few lines later lets the two disagree within a single render.
- **Name what you watch.** `useWatch({ control })` with no `name` re-renders on
every keystroke in every field. Subscribe to the specific names you use.
- `watch(callback)` is deprecated — use `subscribe()` for render-free listeners,
and always return its cleanup from `useEffect`.
// ❌ common in the codebase — all three subscriptions hoist to the form owner
function Fields({ form }: { form: UseFormReturn<FormValues> }) {
form.watch(['storageType', 'totalSize']) // return value discarded
const { errors } = form.formState // prop-form formState
const size = form.getValues('totalSize') // non-reactive read in render
...
}
// ✅ child subscribes for itself and consumes what it watches
function Fields({ control }: { control: Control<FormValues> }) {
const [storageType, totalSize] = useWatch({ control, name: ['storageType', 'totalSize'] })
const { errors } = useFormState({ control })
...
}The canonical form
zod schema → `z.infer` type → `useForm` with `zodResolver` and **complete** `defaultValues` → `<Form {...form}>` → `FormField` render-prop per field → `FormItemLayout` → `FormControl` → primitive from `ui`. Layout/container choices (Card vs Sheet, `layout=` variants) are covered by the `studio-ui-patterns` skill and the demos in `apps/design-system/registry/default/example/` (`form-patterns-pagelayout.tsx`, `form-patterns-sidepanel.tsx`) —
Supabase is the Postgres development platform. We're building the features of Firebase using enterprise-grade open source tools. [x] Hosted Postgres Database. Docs [x] Authentication and Authorization. Docs [x] Auto-generated APIs. [x] REST. Docs [x] GraphQL.
Repo: supabase/supabase
Other skills on supabase.
- /clickhouse-logs-queries
Write, review, and migrate Supabase logs queries against the ClickHouse-backed `logs` table (the `logs.all.otel` analytics endpoint). Use this whenever a task involves Logs Explorer SQL, the `log_attributes` map, querying a log `source` (edge_logs, postgres_logs, auth_logs,
Open skill - /copywriting
Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Load it before shipping or reviewing any user-facing text — including when copy is incidental to the task, like a new feature that adds buttons, toasts, dialogs,
Open skill - /dev-toolbar-review
Safety rules for the dev toolbar, PostHog client, and feature flags. Use
Open skill - /docs-content
Write, edit, organize, and review Supabase content anywhere in apps/docs — guides, explainers, tutorials, troubleshooting entries, reference docs, and partials. Use for MDX/TOML authoring, frontmatter, navigation, terminology, links, code samples, content listings, and docs
Open skill - /safe-sql-execution
Use whenever code will build, return, fetch, or execute SQL that runs against a user's real Postgres database — even when the request reads like an ordinary feature or bug fix and never says "security," "injection," or "SafeSqlFragment." This covers: writing or editing any
Open skill - /studio-e2e-tests
Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
Open skill

