patterntypescriptreactMajor
Autosave forms that stay mounted: uncontrolled fields go stale when the server value changes elsewhere
Viewed 0 times
React 19, Next.js 16 App Router
defaultValueuncontrolled selectstale formautosaveremount keyReact 19
browserssr
Problem
A form that saves field by field (no remount between saves) keeps showing old values in uncontrolled select/input when another control on the page changes the same record (a "complete" button, a bulk edit, a reschedule action). React 19 does not reapply a changed defaultValue to an already mounted select or a dirty input, so the UI contradicts the database, and the next save of a sibling field (time) can send the stale value of another field (day) and silently undo the other change.
Solution
Do not remount the whole form on every version (key={updated_at}) when it autosaves: focus is lost after each save. Instead, in a useEffect keyed on the server values, set el.value = serverValue for each named field that is not the focused element (form.elements.namedItem(name), skip el.ownerDocument.activeElement). For fields held in React state (a controlled select, a derived badge), use the "adjust state when a prop changes" pattern during render (compare with a stored previous server value, then setState). Test with fake form elements and a hook engine that re-renders after a setState made during render.
Why
defaultValue only seeds the DOM value at mount (and React marks inputs dirty), so later prop changes update the value attribute but not what the field shows.
Gotchas
- An sr-only (position:absolute) span inside a table that scrolls in an overflow-x-auto container escapes the container if no ancestor is positioned, and makes the whole page scroll horizontally on small screens; make the cell position:relative.
- After a refusal shown via ?erreur=, a later successful inline save with refresh() keeps the alert; send a hidden flag and redirect to the clean address instead.
Code Snippets
Recaler les champs sans le focus
useEffect(() => {
const form = ref.current; if (!form) return;
for (const [name, v] of [["status", values.status], ["dueDay", values.dueDay]] as const) {
const el = form.elements.namedItem(name) as HTMLInputElement | HTMLSelectElement | null;
if (!el || el.ownerDocument.activeElement === el) continue;
if (el.value !== v) el.value = v;
}
}, [values.status, values.dueDay]);Revisions (0)
No revisions yet.