Action là hàm async được chạy bên trong một transition. React 19 cho startTransition nhận hàm async và cho <form action={fn}> nhận trực tiếp một hàm; trong lúc hàm đó chạy, React tự giữ trạng thái pending và giữ UI cũ hiển thị.
useActionState gom action + state kết quả + cờ pending vào một chỗ:
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const res = await updateName(formData.get('name'))
if (res.error) return { error: res.error } // return, do not throw
return { ok: true }
},
{ },
)
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
)So với cách tự viết bằng useState, khác ở bốn điểm:
- Không còn ba useState rời (loading, error, data) dễ rơi vào trạng thái mâu thuẫn — kết quả là một giá trị trả về duy nhất.
- Pending do React quản lý theo transition, đúng cả khi có nhiều lần submit chồng nhau; React giữ kết quả của lần cuối cùng.
- Form hoạt động trước khi JS hydrate nếu action là Server Action — submit vẫn gửi được, đây là điểm onSubmit tự viết không làm được.
- Ghép sẵn với useFormStatus (nút con tự biết pending) và useOptimistic (giá trị tạm tự rollback khi action kết thúc).
Quy ước quan trọng: trả về lỗi dưới dạng state chứ đừng ném — ném sẽ đẩy lỗi lên Error Boundary và mất luôn form đang nhập dở.