TLDR: React 19 resets uncontrolled form fields after a server action returns — on success and on non-throw failure. The DB write lands fine. Only the UI lies. The fix: controlled state.

the setup

I was building a webinar project-management tool for a hospital, and I needed an assignee dropdown on the template-task editor.

The UX was simple: flip the dropdown, save immediately. No "Save" button. Just an AutoSubmitSelect component that called form.requestSubmit() on onChange.

Felt clean. Felt smart.

the wall

I flipped the assignee. The dropdown snapped back to blank.

Tried again. Same thing. Flip → snap.

My client reported it immediately: "save isn't working."

the wrong turn

My instinct was to check the DB.

I queried template_tasks directly and found default_assignee_id=1 sitting there — already written correctly. Every single row.

The save was not broken. The save was fine.

the actual diagnosis

React 19 resets uncontrolled form fields after a server action returns.

Not just on success. On any non-throw return — which means on failures, on partial errors, on everything except a thrown exception.

It's technically documented in the React 19 release notes. But the docs frame it as something that happens after a "successful submission." In practice it fires unconditionally.

And here's the brutal part: it's invisible to tsc, eslint, and next build. It only shows up when a human clicks in a real browser.

I hit a nastier version of this same bug a day later on a law firm client's law-practice app — specifically their matter-intake form: a server action returned { success: false, error: "CONFLICTS_FOUND" } to surface a confirmation modal — and React reset the entire multi-field form before the modal even rendered. The user clicked "Acknowledge & Save" and submitted a blank form. Cascade.

the fix

Any field that auto-submits on change must be controlled. Three things:

const [value, setValue] = useState(String(defaultValue ?? ""));

useEffect(() => { setValue(String(defaultValue ?? "")); }, [defaultValue]);

onChange={(e) => { setValue(e.target.value); ref.current?.form?.requestSubmit(); }}

Local useState owns the visible value. useEffect syncs it when the server sends a new defaultValue after revalidation. The requestSubmit() fires the action. React can reset whatever it wants — your controlled value stays.

One thing to not do: key={Math.random()} to force a remount. It'll visually fix the flicker but it kills focus and scroll position every time. Controlled state is the right tool.

the diagnostic trifecta

Once you know this gotcha, you can spot it instantly. If you hit all three:

  • DB confirms the write landed — rows have the right value
  • User says the UI "reverts" or "goes back to blank"
  • Page refresh shows the correct value

That's not a save bug. That's React 19 form reset.

I spent too long assuming the action was broken. The trifecta would've cut that chase down to five minutes.

From now on — any field that fires a server action on change gets controlled state from the start, no debate.

P.S. Split auto-submit fields into their own <form> elements. Mixing auto-submit and explicit-save fields in one form multiplies the reset surface in ways that are genuinely hard to reason about.