# React Hook Form Orchestration

> Use when building, refactoring, or reviewing complex React Hook Form flows, especially forms with cross-field dependencies, useWatch/watch, useEffect, setValue, trigger, resolver-driven validation, debounced API calls, dependent quote/price fetches, amount validation, or freeze/infinite-render-loop symptoms.

- Skill: `tianyili/react-hook-form-orchestration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tianyili/react-hook-form-orchestration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tianyili/react-hook-form-orchestration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tianyili (https://skillmd.com/u/tianyili)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tianyili/react-hook-form-orchestration

---


# React Hook Form Orchestration

## Core Rule

Keep cross-field form orchestration in one controller/container layer. Form item components render fields and call explicit handlers; they do not own a distributed form state machine.

## Apply This Pattern When

- A field change resets or defaults another field.
- A field change must revalidate another field.
- A watched value triggers an API call, debounced mutation, or quote fetch.
- Validation depends on derived max/min values, token decimals, balances, or quote output.
- A form freezes or loops after typing into a field.
- A component has `useWatch`/`watch` plus `useEffect` plus `setValue` or `trigger`.

## Required Structure

Follow a feature-module structure that separates orchestration from fields:

- Put orchestration in a container/controller component, not in `components/formItems/*.form.tsx`.
- Keep `formItems/*.form.tsx` field-only: render inputs/selects/buttons, read local field state if needed, and call props callbacks.
- Put options/constants in `constants/`.
- Put reusable pure logic in `utils/`; no async utilities.
- Put non-reusable one-off form logic in the controller, not in module-level hooks.

## Avoid

Do not put this pattern in form items:

```tsx
const value = useWatch({ control, name: 'some_field' });

useEffect(() => {
  form.setValue('other_field', nextValue, { shouldValidate: true });
  form.trigger('third_field');
}, [value, form]);
```

This creates a hidden graph:

`watch -> render -> effect -> setValue/trigger -> formState update -> render`

It becomes especially fragile when the watched result is used as an effect dependency. React Hook Form documents `watch`/`useWatch` results as render-phase optimized; use external comparison when they must drive effects.

## Preferred Pattern

Make dependent transitions event-driven:

```tsx
function handleTargetChainChange(chainId: string) {
  const asset = getDefaultTargetAsset(chainId);

  form.setValue('target_chain', chainId, {
    shouldDirty: true,
    shouldValidate: true,
  });
  form.setValue('target_asset', asset, {
    shouldDirty: true,
    shouldValidate: true,
  });

  if (form.getValues('recipient')) void form.trigger('recipient');
}
```

Then pass the handler into a field-only form item:

```tsx
<TargetChainSelect
  value={targetChainId}
  onChange={handleTargetChainChange}
/>
```

## Quote/API Effects

Keep async effects in the controller and make them key-driven:

```tsx
const quoteRequest = useMemo(() => {
  if (!canQuote) return null;
  return {
    key: [chain, token, amount, recipient].join('|'),
    params,
  };
}, [canQuote, chain, token, amount, recipient]);

useEffect(() => {
  if (!quoteRequest) return;
  const timer = setTimeout(() => fetchQuote(quoteRequest.params), 500);
  return () => clearTimeout(timer);
}, [quoteRequest, fetchQuote]);
```

Do not scatter quote fetching across form items. Do not let quote updates immediately trigger broad validation unless a derived validation key actually changed.

## Validation

- Prefer validation based on form values and explicit derived controller state.
- If validation max/min changes, revalidate by a stable primitive key, not by object identity.
- Avoid render-time ref side channels when possible. If a resolver must read a ref, write it in the controller and keep revalidation guarded.
- Avoid `setValue(..., { shouldValidate: true })` inside effects caused by form subscriptions.

## Review Checklist

Reject or refactor when:

- `components/formItems/*.form.tsx` imports `useWatch`, `watch`, `useEffect`, `setValue`, or `trigger` for cross-field behavior.
- A form item both reads one field and mutates another.
- A dependency array includes the whole `form` object while the effect calls `setValue` or `trigger`.
- A debounced API call watches many fields inside a form item.
- Resolver behavior depends on values mutated during child render.

Accept when:

- One controller owns `useForm`, watched values, derived values, dependent-field handlers, API effects, and submit disabled state.
- Form items are dumb render components.
- Cross-field transitions run from explicit user-event handlers.
- Effects are keyed by stable primitive strings/numbers and guarded against repeated state writes.


