TLDR: In React 19, <form action={serverFn}> resets uncontrolled inputs after the action returns — even when the save succeeded. Make them controlled with useState + a useEffect that syncs from defaultValue. Without the useEffect, you just trade one bug for a staler one.

The Feature

I was building inline task editing for a webinar project-management tool I built for an ecommerce business client — Things 3-style, where clicking a task row lets you edit the title, date, and assignee right there in the row, no slide-out panel.

The assignee field used AutoSubmitSelect — a <select> wired to <form action={serverFn}> that fires requestSubmit() on change.

Clean pattern. Should just work.

What Broke

It didn't look like it worked.

Flip the assignee dropdown, it snapped straight back to the previous value.

I went to verify the DB. Queried template_tasks directly.

default_assignee_id=1. The write landed.

So the data was correct. The screen was lying.

What Was Actually Happening

React 19 does this: after <form action={serverFn}> fires and awaits, it calls revalidatePath and then resets every uncontrolled input back to its defaultValue.

The rerender with the new server value is in-flight. But there's a brief window where the input has snapped back first.

To a user? It reads as "save didn't work."

It did. React just reset the visual state before the fresh data arrived.

Fix #1 — Make It Controlled

Easy fix: give the select a controlled value + useState.

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

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

Reset is gone. The selection holds.

But this opened a second bug. Local state is now the source of truth. If the server returns a different value — a fresh revalidate, a concurrent save, a normalized value — the input ignores it. It's stuck on whatever the user last touched.

Fix #2 — The Re-Sync

The useEffect is the actual fix.

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

Now the component is optimistic in the interaction (local state responds instantly) but defers to the server when defaultValue changes after revalidation.

That's the arc: the user's click feels immediate, and the server's truth wins when it arrives. No flicker on success. No stale display on a concurrent update.

The full pattern — controlled value + useState + useEffect sync on the server prop — works for any field type. Title inputs, date pickers, assignee dropdowns. Same 3 lines.

Why This Matters to Me

I spent real time questioning whether the DB writes were even landing.

They were.

The data was right the whole time. The UI was just lying about it — because I trusted that <form action> would behave like every form I'd written before React 19.

It doesn't. And once you know that, the fix is dead simple.

Control your inputs. Sync from the server prop. Let the server win when it has something to say.