Handling Forms via Form ActionsEntry 7 of 10

Multiple Submit Buttons with formAction

A <button formAction={fn}> overrides the form's action for that button, so one form can trigger different actions.

Notes

  • Put the actions on the buttons instead of the <form>. Each button submits the same FormData to its own function.
  • Typical uses: upvote / downvote, Save draft / Publish, Approve / Reject.
  • Each action can come from its own useActionState call if it needs its own state and pending flag.
  • useFormStatus().action tells a child which action is running.

What / Why / When

What

Per-button actions in one form.

Why

No hidden "intent" field or branching inside one handler.

When

A form whose buttons do different things with the same data.

Use case

Upvote and downvote buttons on an opinion.

Code compare

React
// Opinion.tsxtype OpinionProps = {  opinion: { id: string; title: string; body: string; userName: string; votes: number };};​export function Opinion({ opinion: { id, title, body, userName, votes } }: OpinionProps) {  function upvoteAction() {    console.log("UPVOTE", id);  }​  function downvoteAction() {    console.log("DOWNVOTE", id);  }​  return (    <article>      <header>        <h3>{title}</h3>        <p>Shared by {userName}</p>      </header>      <p>{body}</p>      <form className="votes">        <button formAction={upvoteAction} aria-label="Upvote">          ▲        </button>        <span>{votes}</span>        <button formAction={downvoteAction} aria-label="Downvote">          ▼        </button>      </form>    </article>  );}

Your note's screenshot destructures nested props: { opinion: { id, title, … } } pulls fields out of the opinion prop in one step.

Two buttons with their own click handlers; type="button" keeps them from triggering (ngSubmit):

Angular
@Component({  selector: "app-opinion",  template: `    <article>      <header>        <h3>{{ opinion().title }}</h3>        <p>Shared by {{ opinion().userName }}</p>      </header>      <p>{{ opinion().body }}</p>      <div class="votes">        <button type="button" (click)="upvote()" aria-label="Upvote">▲</button>        <span>{{ opinion().votes }}</span>        <button type="button" (click)="downvote()" aria-label="Downvote">▼</button>      </div>    </article>  `,})export class OpinionComponent {  opinion = input.required<Opinion>();​  upvote() {    console.log("UPVOTE", this.opinion().id);  }  downvote() {    console.log("DOWNVOTE", this.opinion().id);  }}

Differences and gotchas

  • Buttons with formAction still submit the form, so the browser runs built-in validation first.
  • Without a form at all, a plain onClick works too. The form version is what makes useFormStatus and useOptimistic work for these buttons.

Quick reference

React 19Angular
<button formAction={upvoteAction}><button type="button" (click)="upvote()">
One form, several actionsOne template, several methods