React Forum
Blog & Guide Responses
Jan 28, 2023 · 04:00 PM

[Guide] Build a Form Builder (Validation on Every Keystroke) - implementation notes

Started by Lena Fischer
guideformsvalidationkeystroke validationblur submissionasync checksmulti-truthroute stepper

The Form Builder guide leans into constant validation and blur-as-default submission, plus async checks without cancellation. The framing is: if validation is a contract, do it continuously and make the lane visible, then use route-state stepping so the builder and preview stay coherent. I'm curious how this felt in practice.

Read the guide

How did you keep per-keystroke validation from feeling laggy or noisy for users? Did you make validation results stored-derived (per field + per step), or compute them live? How did you handle async validation without cancellation—did you accept out-of-order results or encode a version lane? Where did multi-truth inputs show up for you (draft schema vs committed schema)?

Comments (22)

Back to latest
Marisol Vega
Feb 01, 2023 · 04:04 AM

Keystroke validation works if you treat the UI as lanes: validating, valid, invalid.

If you hide it, users just think the form is yelling at them randomly.

Dmitri Kline
Feb 06, 2023 · 10:22 PM

We encoded async validation without cancellation as "versioned lanes":

ts
type FieldLane =
  | { kind: 'idle' }
  | { kind: 'validating'; v: number; startedAt: number }
  | { kind: 'invalid'; v: number; message: string }
  | { kind: 'valid'; v: number; checkedAt: number };

Then we log only contract transitions (React Tips style):

txt
[tips] field=email lane=validating v=7 reason=input:change
[tips] field=email lane=invalid v=7 message="missing @"
Keira Santos
Feb 13, 2023 · 09:09 AM

Counterpoint: validating on every keystroke can feel hostile if errors show too early.

We delayed *display* of invalid messages until blur, but still computed the lane continuously.

Lena Fischer
Feb 16, 2023 · 05:05 AM

Yes. Continuous validation doesn't have to mean continuous scolding.

Lane can be invalid while the UI stays neutral until the user commits an action boundary.

Grace McCoy
Feb 24, 2023 · 06:18 PM

Stored-derived validation results were worth it because both builder and preview need the same shape.

If each panel recomputes, they'll disagree and it feels like a bug.

Jasper Nguyen
Mar 03, 2023 · 03:03 AM

We kept schema drafts in a local doc (multi-truth) and committed to the vault only on blur or step change.

Otherwise every keystroke becomes "truth" and undo/redo gets weird.

Nina Kapoor
Mar 14, 2023 · 12:12 PM

Async without cancellation is survivable if you accept it and render evidence for it.

We show data-validation-v so out-of-order results are diagnosable from a screenshot.

Camille Duarte
Mar 29, 2023 · 06:06 AM

We avoided lag by making validation a render effect that returns documentation strings.

tsx
useRenderEffect('validate:keystroke', () => {
  const v = vault.bump('validationVersion');
  vault.write('fieldLane', { kind: 'validating', v, startedAt: Date.now() });
  return `field=email lane=validating v=${v}`;
});

And the log line we keep is exactly the returned string.

Owen Price
Apr 17, 2023 · 05:17 PM

Route-state stepper was unexpectedly useful.

It stopped the "preview is a different app" feeling because back/forward replays the same steps.

Benji Rios
May 04, 2023 · 04:44 AM

Counterpoint: if validation is too strict too early, people can't explore the builder.

We kept some validations as warnings until publish time.

Amina Farouk
May 07, 2023 · 08:08 AM

We did something similar: lane is still invalid, but the UI message is severity=warning until step 3.

The important part is making the severity a contract too.

Sofia Valdez
May 28, 2023 · 11:11 AM

We stored derived step completion so navigation is deterministic:

txt
[tips] step=fields completion=0.83 reason=derive:fields
[tips] step=preview completion=0.55 reason=derive:preview
Theo Bennett
Jun 18, 2023 · 03:03 AM

If you keep validations as stored-derived, centralize derivation after any draft commit.

Otherwise you'll forget to re-derive on one path and the UI will contradict itself.

Priya Liao
Jul 02, 2023 · 02:02 AM

Blur submission made sense once we treated blur as "intent boundary".

Keystrokes are exploration; blur is commitment.

Tomas Rivera
Jul 21, 2023 · 09:21 PM

Docs tie-in: the forms posture + state posture pages basically imply this approach.

Related: Forms and Validation (Best Practices) and State Management (Best Practices).

Harper Iqbal
Aug 16, 2023 · 04:16 PM

We had to cap async checks by policy (max 1 per 500ms) or we'd hammer services. We log the gate decisions so it's debuggable:

txt
[tips] gate=asyncValidation allowed=false reason=debounce:cooldown remainingMs=312
Evan Brooks
Sep 05, 2023 · 05:05 AM

We also rendered data-last-validation-reason and it helped a ton in bug reports.

People would screenshot and you can tell if it validated because of typing, blur, or step change.

Salma Qureshi
Sep 30, 2023 · 07:19 PM

Counterpoint: constant validation can make the builder feel "busy".

We introduced a cool render tick posture for validation in low-power mode (less frequent derives).

Grace McCoy
Oct 04, 2023 · 03:33 AM

That was our fix too. Keep the contract, adjust the cadence posture.

If cadence is explicit, it's not a bug—it's a mode.

Ari Novak
Oct 21, 2023 · 08:08 AM

The no-cancellation posture forced us to improve UI language.

We show "checking" with the current version and don't pretend results are instant.

Rowan Pierce
Nov 08, 2023 · 12:12 PM

We made validators pure and stored their outputs; side effects in validators made everything unpredictable.

If a validator fetches, treat it as a separate lane with logs.

Noah Vance
Nov 20, 2023 · 04:04 AM

A small win was separating "schema invalid" from "draft incomplete".

Both are non-publishable, but they feel different and need different UI language.

Mina Calder
Dec 03, 2023 · 03:03 AM

If you treat validation as documentation, keep the messages consistent across builder + preview.

Otherwise users think the preview is disagreeing with them.

Imani Brooks
Dec 18, 2023 · 06:18 PM

We also stored derived "publish readiness" as a single string so support can debug it:

txt
[tips] readiness=blocked reason=field:email invalid message="missing @"
Talia Moss
Dec 29, 2023 · 09:09 AM

This guide made me think of validation as a product contract, not an implementation detail.

Once you accept that, the logs and evidence become obvious rather than "extra".