React 19 useActionState Not Resetting Form After Success

React 19 useActionState returns success but the form shows old input values. Here is why the state persists and the form-key pattern that resets it cleanly.
React 19 useActionState Not Resetting Form After Success
ReactNext.jsServer Actions
June 2, 20266 min read1046 words

The Problem

I ran into this on a client project last week. A simple contact form using useActionState with a Next.js Server Action. The server returns { ok: true, message: 'Sent' }, the success state renders, the toast fires, and then the form fields still show whatever the user just typed. Hit submit again and the same payload goes out a second time.

The user-visible bug is worse than it sounds: a confused customer clicks the now-green "Send" button again because nothing looks like it changed, and you get duplicate inquiries hitting Resend, Slack, or whatever you wired into the action. Two of my client's sales emails had four duplicate "Send another?" submissions in the last week before they flagged it.

Calling form.reset() from inside the action callback doesn't work either. The form has already been re-rendered by the time you'd run it, and on App Router that callback fires on the server, so there is no form to call .reset() on.

Why It Happens

useActionState from React 19 returns the latest action result and a pending boolean, but it deliberately does not touch the DOM form state. The hook's job is to surface what the server returned, not to manage controlled input values. React 18's useFormState had the same gap, but it lived in react-dom and people generally used it with formAction on a button, where browsers auto-reset the form on submit. React 19 promoted it into core and changed the default: forms no longer auto-reset when an action runs, because too many real-world apps wanted to keep input values around for editing flows.

The result is that successful submissions look identical to the "still typing" state. The inputs are uncontrolled, the browser kept their values across the action round-trip, and React has no reason to clear them. From the React docs on form actions, the official guidance is to either pass a function to action that calls reset() yourself, or to remount the form when you want a clean slate.

The other thing that bites people: if you check state.ok inside a useEffect and call ref.current?.reset(), it works the first time and then breaks. The reason is that state is the same object reference the second time the action returns the same shape. React's effect doesn't re-run, the reset doesn't fire, and you are back to stale fields.

The Fix

The cleanest pattern is to key the form on a value that changes after every successful submit. React unmounts the old form and mounts a fresh one with default values. No ref juggling, no effect dependencies to get wrong.

Step 1: Return a submission counter from your action. Bump it on every successful run:

// app/contact/actions.ts
'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string().min(10),
});

export type ContactState = {
  ok: boolean;
  message: string;
  submittedAt: number;
};

export async function submitContact(
  _prev: ContactState,
  formData: FormData
): Promise<ContactState> {
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    return {
      ok: false,
      message: parsed.error.issues[0].message,
      submittedAt: 0,
    };
  }

  await sendToResend(parsed.data);

  return {
    ok: true,
    message: 'Thanks, I will be in touch.',
    submittedAt: Date.now(),
  };
}

The submittedAt value is the trick. It changes on every successful call, which gives the client component a stable signal that something actually happened.

Step 2: Key the form on that timestamp. When submittedAt changes, React remounts the form with empty defaults:

// app/contact/contact-form.tsx
'use client';

import { useActionState } from 'react';
import { submitContact, type ContactState } from './actions';

const initial: ContactState = { ok: false, message: '', submittedAt: 0 };

export function ContactForm() {
  const [state, action, pending] = useActionState(submitContact, initial);

  return (
    <form key={state.submittedAt} action={action}>
      <input name="name" required defaultValue="" />
      <input type="email" name="email" required defaultValue="" />
      <textarea name="message" required defaultValue="" />
      <button disabled={pending}>{pending ? 'Sending…' : 'Send'}</button>
      {state.message && (
        <p role="status" data-ok={state.ok}>{state.message}</p>
      )}
    </form>
  );
}

Three things matter here. key={state.submittedAt} is the reset mechanism. defaultValue="" makes the inputs uncontrolled, so controlled inputs would need their own state and a manual clear. And data-ok={state.ok} gives the styling layer something to hook into for the success message without changing the form key on validation errors.

Failed submissions return submittedAt: 0, so the key doesn't change, the form stays mounted, and the user's typed values stick around. That is what you want for "fix this email and resubmit" flows.

Step 3: If you have controlled inputs, reset them in the action prop. Sometimes you need controlled state for live validation. In that case, wrap the action and call your reset function alongside the dispatch:

const formRef = useRef<HTMLFormElement>(null);
const [name, setName] = useState('');

async function handleSubmit(formData: FormData) {
  const result = await submitContact(state, formData);
  if (result.ok) {
    setName('');
    formRef.current?.reset();
  }
  return result;
}

const [state, action, pending] = useActionState(handleSubmit, initial);

This drops the form-key trick and gives you full control, at the cost of having to remember to clear every piece of state yourself. For anything with more than two or three fields, the keyed-form pattern is less error-prone.

The Lesson

React 19 stopped auto-resetting forms after actions, and useActionState does not touch input DOM. If you want a clean form after a successful submit, key the form on a value that changes after every success — a server-returned timestamp is the simplest one. Don't rely on useEffect against state.ok, and don't try to call .reset() from inside the server action.

If your contact or signup form is sending duplicates because the post-submit UX is muddled, that is the kind of small fix I roll into client engagements. See my services. For a related Server Action pitfall, read React useFormStatus pending stuck on server action.

Forms sending duplicates after success? Let me fix it.

Back to blogStart a project