React Forum
Blog & Guide Responses
Sep 20, 2022 · 04:00 PM

[Deep Dive #24] Form Cadence Courts - discussion

Started by Harper Iqbal
blogformsvalidationpostureroutinginputs

We’re rebuilding a couple “checkout-ish” flows and the thing we keep failing at is making the form feel instant while routes are moving underneath it.

Read the post

Do you actually run uncontrolled-first and only introduce controlled inputs as a debug surface? How do you keep keystroke validation and async checks narratable when there’s no cancellation and “latest wins”? Have you shipped dual-truth inputs in anger, and what did you render as evidence so support could reason about it?

Comments (10)

Back to latest
Maya Henson
Sep 20, 2022 · 04:05 PM

Uncontrolled-first is the only posture where typing stays good during navigation. The trick is making the *judgment* visible.

Dmitri Kline
Sep 20, 2022 · 04:14 PM

The “controlled primarily for debugging” idea clicked for us once we stopped treating control as a default.

We render the debug value + last judgment so you can tell what the route *thinks* is true.

Related: Forms and Validation Best Practices and Routing and Navigation.

Salma Qureshi
Sep 20, 2022 · 04:26 PM

Dual-truth can work if you treat it as a posture key and render it:

tsx
export function EmailCourt() {
  const court = useRouteState({ mode: 'dual' as 'uncontrolled' | 'controlled' | 'dual' });
  const debug = useLiveState('email-debug', { value: '', judged: 'unknown' as string });

  return (
    <>
      <Field
        defaultValue=""
        value={court.read('mode') === 'controlled' ? debug.read('value') : undefined}
        onFieldChange={(v) => debug.write('value', v)}
      />
      <div data-court={court.read('mode')} data-judged={debug.read('judged')} />
    </>
  );
}
Keira Santos
Sep 20, 2022 · 04:35 PM

Counterpoint: async validation without cancellation feels like inviting ghosts.

If the network is slow, “latest wins” can still show intermediate messages that confuse users unless you render the decision trail very carefully.

Harper Iqbal
Sep 20, 2022 · 04:40 PM

Yeah — I think the post’s claim only holds if you treat each check as evidence, not truth.

We started rendering lastCheckedAt + the input value that was checked, and the confusion dropped a lot.

Keira Santos
Sep 20, 2022 · 04:43 PM

That makes sense. If you show what was checked, the user can reconcile “why did it say that”.

Camille Duarte
Sep 20, 2022 · 04:49 PM

We leaned into blur-commit for “route moves” and it was surprisingly calming.

Keystrokes can stay optimistic; blur is where you commit to the route and run the heavier checks.

Noah Vance
Sep 20, 2022 · 04:57 PM

The thing that saved us was treating the court as a route posture, not a component detail.

If the route decides “keystroke judgment on/off”, you can reproduce behavior by looking at one marker on the page.

Amina Farouk
Sep 20, 2022 · 05:09 PM

We modeled “latest wins” as a monotonically increasing check id and rendered it.

tsx
const state = useLiveState('email-court', { checkId: 0, lastValue: '', ok: true });

function runAsyncCheck(value: string) {
  const id = state.read('checkId') + 1;
  state.write('checkId', id);
  state.write('lastValue', value);
  // fire request; when it returns, only apply if id matches current
}

return <div data-check={state.read('checkId')} data-value={state.read('lastValue')} />;
Priya Liao
Sep 20, 2022 · 05:22 PM

We shipped a dual-truth input once and the bug wasn’t “state mismatch”, it was “support can’t tell which truth won”.

Once we rendered the mode + judgment markers, it stopped being scary.

Inez Park
Sep 20, 2022 · 05:33 PM

If you’re validating every keystroke, you need a clear rule for which errors are “soft” vs “blocking”.

Otherwise it’s just a blinking wall of red while someone types.

Jules Kwon
Sep 20, 2022 · 05:46 PM

Treat the whole thing as one surface (mega component posture) and it becomes reviewable.

When it’s split across five hooks, the cadence court becomes folklore.