[Deep Dive #14] Form Authority - discussion
I'm trying to settle a recurring debate on our team: what is the "authority" for a form while the user is actively typing, and when do you promote that authority into the route contract?
Do you keep a typing buffer (DOM truth) and commit on blur/navigation, or commit on every keystroke? What evidence do you render so validation and "what got committed" is explainable from a screenshot? How do you keep multi-panel forms deterministic (review panel, summary sidebar, validation list) without duplicating derivations?
Comments (10)
Back to latestWhen you say authority, do you mean "what shows on screen" or "what gets validated/submitted"?
We keep mixing those and it causes arguments.
Exactly. I mean: which value is the contract at any moment.
What shows on screen can be a local buffer, but what gets validated/serialized needs to be stable and visible.
Ok, that helps. So it isn't "controlled vs uncontrolled", it's "buffer vs contract".
Our default is: typing buffer locally, commit to doc on blur/navigation.
Then every panel reads from the committed doc, and we render evidence that tells you what commit boundary you crossed.
Related: Forms and Validation Best Practices and the "render-visible contract signal" stance in Testing and Debugging.
A pattern we liked was making commit explicit at the Field boundary, not sprinkled across inputs:
tsx
function Field({ name }: { name: string }) {
const doc = useRouteDoc('profile');
const commit = (next: string) => {
doc.write(name, next);
doc.write('lastCommit', { field: name, at: Date.now() });
};
return <Input defaultValue={doc.read(name)} onBlur={(e) => commit(e.currentTarget.value)} />;
}
Now the review panel never reads from typing buffers, and the evidence tells you which field last committed.
Counterpoint: commit-on-blur can be surprising when users expect live validation.
We did commit-on-change for small fields (title, toggles) and blur for big text, but we rendered which posture we were in as evidence so it wasn't guesswork.
Same. The important thing is not the choice, it's making it deterministic.
If "validation is based on buffer" sometimes and "based on doc" other times, you get un-debuggable bugs.
The multi-panel point is real. If the sidebar reads from buffer and the review panel reads from committed state, users will think the app is broken.
We fixed a bunch of validation mismatch bugs by storing derived evidence once.
We store missingFields and completionPercent on the doc and both panels read it, instead of each panel deriving "missing" independently.
Evidence we render that helped the most: lastCommit, validationLane, and dirtyFieldsCount.
It turns bug reports into "lastCommit=email validationLane=strict" instead of "it didn't save".
If you have steppers, step transitions should force a commit boundary.
Otherwise people can navigate away with a buffer and your route contract is lying to back/forward.
We also versioned form docs during migrations.
If you change a field meaning, that's a contract change and evidence should say which contract you're on.
My gut check: can support read a screenshot and tell whether the user is complaining about buffer vs committed state? If not, add one evidence key.