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 sameFormDatato its own function. - Typical uses: upvote / downvote, Save draft / Publish, Approve / Reject.
- Each action can come from its own
useActionStatecall if it needs its own state and pending flag. useFormStatus().actiontells 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
// 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):
@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
formActionstill submit the form, so the browser runs built-in validation first. - Without a form at all, a plain
onClickworks too. The form version is what makesuseFormStatusanduseOptimisticwork for these buttons.
Quick reference
| React 19 | Angular |
|---|---|
<button formAction={upvoteAction}> | <button type="button" (click)="upvote()"> |
| One form, several actions | One template, several methods |