Ask
58

form fires my server action twice on slow connections and useFormStatus stays false Mutations

<form action={createOrder}> with a submit button that also has an onClick wrapping the same action in startTransition. On wifi you never see it. On a throttled 3G profile I get two order rows about 40% of the time. And useFormStatus() returns pending: false for the whole submit, so the disabled state never applies and the user just keeps clicking.

7 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @gpio_gwen · 3d ago · 2 replies

    Two separate bugs stacked on each other.

    1. You are literally submitting twice. The form action fires and your onClick fires. Delete the onClick — the form already calls it with the FormData.
    2. useFormStatus reads the status of the form it is inside. If you call it in the same component that renders the <form>, it has no parent form to read and you get pending: false forever. Pull the button into <SubmitButton /> and call the hook there.

    After that, still add an idempotency key. A user on a bad connection will double submit no matter how good your disabled state is.

    88
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @pentalobe_junie · 5d ago

      The onClick was left over from before I moved to a form action, and I had read straight past it four times. Moved the button into its own component and pending flips correctly now.

      21
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @iron_camber · 3d ago

    useActionState is the simpler shape for this — you get isPending in the same component that renders the form, no child component needed, and you get the returned error state in the same place.

    One subtlety people trip on: pending goes false when the action returns, not when the revalidated UI arrives. If your list takes 300ms to come back you get a gap where the button is enabled and the row isn't there yet. That gap is what useOptimistic is for.

    51
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @wild_ferment · 3d ago · 2 replies

    Client-side guards are UX. The correctness fix is a unique index. Generate a key when the form renders, send it as a hidden field, unique (user_id, idempotency_key) in Postgres, catch 23505 and return the existing order. Now it does not matter how many times the button is pressed or by whom.

    34
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @nadia_brill · 5d ago

      This also covers the cases you cannot see from the client: the user restoring a tab, a retry from a flaky proxy, someone with a trackpad that double-fires. Any mutation that moves money gets a key on my projects, no discussion.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @kitchen_table_talk · 5d ago · 2 replies

    Debounce the submit 300ms and move on. Most double submits are physical double clicks.

    12
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @gradschool_gary · 5d ago

      Debounce hides the click-happy case and does nothing for the 3G case, where the second submit arrives four seconds later because nothing on screen changed. The pending indicator plus the unique index is the pair that actually closes it.

      8
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report