Overview

Submit a server action that returns ActionResponse, show success/error with ActionMessageToast, and keep UX consistent via TransitionButton.

'use client'

import * as React from 'react'
import { TransitionButton } from '@/components/TransitionButton'
import { ActionMessageToast } from '@/components/ActionMessageToast'
import { updateUserProfileAction } from '@/actions/updateUserProfileAction'

export function ProfileForm() {
  const [isPending, startTransition] = React.useTransition()

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    startTransition(async () => {
      try {
        const res = await updateUserProfileAction(formData) // ActionResponse
        if (res?.success) ActionMessageToast.success('Profile updated')
        else ActionMessageToast.error(res?.error ?? 'Update failed')
      } catch {
        ActionMessageToast.error('Unexpected error')
      }
    })
  }

  return (
    <form onSubmit={onSubmit} className="grid gap-4">
      <input name="name" placeholder="Your name" />
      <TransitionButton type="submit" isPending={isPending}>
        Save changes
      </TransitionButton>
    </form>
  )
}

Key conventions

  • Server actions return ActionResponse ({ success: boolean; error?: string; data?: T }).
  • Use React.useTransition() — not useState — so the transition blocks concurrent renders while the action is in flight.
  • TransitionButton accepts isPending and renders a spinner automatically.
  • ActionMessageToast wraps the shadcn toast in a single-call API (success, error, info).
  • Wrap the startTransition body in try/catch to surface network-level failures separately from action errors.