javascriptreactjsformsnext.jsserver-action

Controlled Select Element Loses Value After Form Submission with useActionState in Next.js


I'm building a form—ideally using the useActionState server-action setup—that needs to persist its values during submissions. The problem is, I can't use the defaultValue prop on the element, because I need it to be a controlled component so I can update it dynamically on the client side.

My current setup

I tried making the controlled using local state, but then the server state doesn't apply correctly after submission. It resets the select value to be the first option, even though the server state and local client state both have most up to date values. Upon submitting again, it then submits that first incorrect select option ("Mini").

What can I do in this situation? Move to javascript query selectors?

I left a very simple example of my problem below.

Form:

"use client";

import { useActionState, useEffect, useState } from "react";
import { saveFormData } from "./actions";

type FormState = {
  group: string;
};

const defaultState: FormState = {
  group: "Junior",
};

export default function FormExample() {
  const [state, formAction] = useActionState(saveFormData, defaultState);

  const [group, setGroup] = useState(state.group);

  useEffect(() => {
    if (state.group !== group) {
      setGroup(state.group);
    }
  }, [state.group]);

  return (
    <form action={formAction}>
      <label>
        Group:
        <select
          name="group"
          value={group}
          onChange={(e) => setGroup(e.target.value)}
        >
          <option value="Mini">Mini (11–12)</option>
          <option value="Junior">Junior (12–14)</option>
          <option value="Senior">Senior (15+)</option>
        </select>
      </label>

      <button type="submit" className="btn">
        Submit
      </button>
    </form>
  );
}

Server action:

"use server";

export async function saveFormData(prevState: unknown, formData: FormData) {
  const group = String(formData.get("group"));

  console.log("group on server: ", group);
  
  // Persist values here (e.g., database, cookies, etc.)
  return { group };
}

Solution

  • This is a confirmed react 19 bug affecting the controlled <select> elements. It has introduced automatic form reset behavior that calls form.reset() after successful submissions. This creates a race condition specially with controlled <select> elements. Here is the GitHub issue

    1. Form submits -> useActionsState batches state update
    2. React's automatic reset fires -> Select DOM element reset to first option
    3. Reconsiliation runs -> But DOM was already reset, causing desynchronization.

    You can use key attribute to force remount. Also since we want to force synchronization every time state.group changes, simple approach without comparison is actually better inside the useEffect

    "use client";
    
    import { useActionState, useEffect, useState } from "react";
    import { saveFormData } from "./actions";
    
    export default function FormExample() {
      const [state, formAction] = useActionState(saveFormData, { group: "Junior" });
      const [group, setGroup] = useState(state.group);
    
      useEffect(() => {
       setGroup(state.group); // Always sync, no comparison needed
      }, [state.group]);
    
      return (
        <form action={formAction}>
          <label>
            Group:
            <select
              key={`${state.group}-${Date.now()}`} // Force remount on each server update
              name="group"
              value={group}
              onChange={(e) => setGroup(e.target.value)}
            >
              <option value="Mini">Mini (11–12)</option>
              <option value="Junior">Junior (12–14)</option>
              <option value="Senior">Senior (15+)</option>
            </select>
          </label>
    
          <button type="submit" className="btn">
            Submit
          </button>
        </form>
      );
    }