useActionState는 async 함수(액션)를 실행하고, 그것이 반환하는 것을 state로 저장하며, 아직 실행 중인지 알려 주는 React 19 훅입니다. const [state, formAction, isPending] = useActionState(action, initialState)로 호출하고 formAction을 <form>에 넘깁니다.
먼저 빈 폼을 제출해 보세요. 액션이 오류 객체를 반환하고 메시지가 나타납니다. 그다음 @가 들어간 주소를 입력하고 다시 제출하세요. 1초 동안 버튼이 "Subscribing..."으로 표시되고 비활성화됩니다.
문법
const [state, formAction, isPending] = useActionState(action, initialState, permalink?);
action은 실행할 함수입니다. React는 이것을action(previousState, ...args)로 호출합니다. 반환하는 값(async라면 완료된 값)이 새state가 됩니다.initialState는 액션이 한 번도 실행되기 전의 state입니다. 컴포넌트가 첫 렌더링에서도state.message를 읽을 수 있도록 액션이 반환하는 것과 같은 모양을 쓰세요.state는 최신 결과입니다.formAction은 액션을 감싼 버전입니다.<form action>이나<button formAction>에 넘기거나 직접 호출하세요.isPending은 액션이 시작된 순간부터 끝날 때까지true입니다.permalink는 선택 사항이며 프레임워크의 서버 함수에만 의미가 있습니다. 자바스크립트가 로드되기 전에 폼이 제출되면 이동할 URL입니다.
액션은 이전 state를 첫 번째 인자로 받습니다. 폼을 옮길 때 가장 흔한 실수입니다. async function save(formData)라고 쓰면 formData에 이전 state가 들어오고 formData.get이 오류를 던집니다.
FormData로 폼 읽기
formAction을 폼의 action으로 쓰면 React가 폼의 FormData와 함께 액션을 호출합니다. name을 가진 모든 필드가 그 안에 있습니다. input을 위한 useState도, onChange 핸들러도 필요 없고, e.preventDefault()도 호출하지 않습니다. React가 submit 이벤트를 처리합니다.
Spicy를 해제하고 다시 제출해 보세요. 체크되지 않은 체크박스는 아무것도 보내지 않으므로 기록된 데이터에서 spicy 키가 사라집니다. 액션은 하나를 더하기 위해 previousState.count도 읽으며, 그래서 이전 state가 먼저 옵니다.
유효성 검사 오류 반환하기
유효성 검사는 액션에 속합니다. 오류를 state의 일부로 반환하고, 폼이 다시 채울 수 있도록 사용자가 입력한 값도 반환하세요. 폼 액션이 끝나면 React가 폼의 비제어 필드를 초기화하므로, defaultValue={state.values...}가 없는 필드는 제출이 실패한 뒤 비어서 돌아옵니다.
한 글자짜리 이름과 짧은 비밀번호를 입력하고 제출해 보세요. 두 오류가 모두 나타나고, 두 필드 모두 입력한 내용을 유지합니다. defaultValue={state.values.name}을 지우고 다시 제출하면 오류 후에 이름 필드가 비워집니다.
실제 앱에서는 최종 판단을 서버에 맡기세요. 클라이언트 검사는 빠른 피드백을 주지만, 누구든 폼 없이 요청을 보낼 수 있습니다.
폼 바깥에서 액션 호출하기
formAction은 함수이므로 버튼이나 이펙트에서도 호출할 수 있습니다. 직접 호출할 때는 startTransition으로 감싸세요. 폼은 그것을 대신해 주지만 일반 onClick은 그렇지 않으며, 트랜지션이 없으면 isPending이 올바르게 업데이트되지 않습니다(React가 개발 환경에서 이에 대한 오류를 기록합니다).
Load more를 몇 번 클릭해 보세요. 각 호출은 이전 호출이 반환한 state에서 시작하므로 페이지가 순서대로 덧붙여집니다. 액션이 아직 실행 중일 때 다시 호출하면, React는 그 호출을 대기열에 넣고 현재 호출이 끝난 뒤 각각 최신 state로 실행합니다.
useActionState는 어디서 왔을까
React 19가 출시되기 전에 이 훅은 canary 빌드에서 react-dom에서 import하는 useFormState로 제공되었습니다. 이름이 useActionState로 바뀌고, react로 옮겨지고, isPending 값을 얻었습니다. 이런 오래된 코드를 발견하면 import와 이름을 고치면 됩니다.
// React canary, before 19
import { useFormState } from 'react-dom';
const [state, formAction] = useFormState(action, initialState);
// React 19
import { useActionState } from 'react';
const [state, formAction, isPending] = useActionState(action, initialState);
깊이 중첩된 제출 버튼에서 대기 상태를 보여 주려면 react-dom의 useFormStatus가 props 없이 부모 폼의 상태를 읽어 줍니다. React 19 개요에서 다른 폼 기능과 함께 소개합니다.
오류, 그리고 다른 것을 써야 할 때
액션이 오류를 던지면 React는 state를 업데이트하는 대신 가장 가까운 에러 바운더리를 보여 줍니다. 유효성 검사나 이미 사용 중인 사용자 이름처럼 사용자가 고칠 수 있는 오류는 가입 예제처럼 state로 반환하세요. 진짜 실패만 던지게 하세요.
useActionState는 액션의 결과를 위한 것입니다. 클릭하면 올라가야 하는 좋아요 수처럼 액션이 실행되는 동안 즉시 업데이트되는 값이라면 useOptimistic과 함께 쓰세요. 키를 누를 때마다 바뀌고 렌더링 중에 읽는 입력에는 여전히 일반 useState와 제어 input이 맞는 도구입니다.
자주 하는 실수
이전 state 매개변수를 잊는 것. async function save(formData)는 formData에 이전 state를 받습니다. 폼에서의 시그니처는 항상 (previousState, formData)입니다.
name이 없는 input. FormData에는 name 속성을 가진 필드만 들어갑니다. id만 있는 필드는 formData.get(...)에서 빠지고 null이 반환됩니다.
모든 곳에 제어 input 쓰기. 폼 액션 안에서도 value와 onChange를 쓸 수 있지만, 그럴 필요는 없습니다. name과 defaultValue를 가진 비제어 필드가 액션에 더 간단하게 맞으며, 제출 후 React의 초기화도 비제어 필드에만 영향을 줍니다.
액션 안에서 state 읽기. 컴포넌트의 state는 핸들러를 만든 렌더링의 값입니다. 액션 안에서는 여러 호출이 대기열에 있더라도 항상 직전 호출의 결과인 previousState를 쓰세요.
예상되는 오류를 던지기. 던진 오류는 폼을 가장 가까운 에러 바운더리로 바꿉니다. 틀린 비밀번호는 충돌이 아닙니다. state로 반환하세요.
자주 묻는 질문
useActionState는 무엇을 반환하나요?
항목 세 개를 가진 배열입니다. 현재 state(액션이 마지막으로 반환한 값, 또는 실행 전의 초기 state), 폼에 넘기거나 직접 호출할 새 액션, 그리고 액션이 실행되는 동안 true인 isPending입니다.
액션은 어떤 인자를 받나요?
먼저 이전 state를, 그다음 액션을 호출할 때 넘긴 것을 받습니다. <form action={formAction}>에서는 그 두 번째 인자가 폼의 FormData입니다.
useActionState와 useFormState의 차이는 무엇인가요?
같은 훅입니다. React canary 버전에서 react-dom의 useFormState로 나왔다가, React 19를 위해 useActionState로 이름이 바뀌어 react로 옮겨졌고, 그때 isPending 값도 얻었습니다.
폼 없이 useActionState를 쓸 수 있나요?
네. 반환된 액션을 startTransition 안에서 직접 호출하세요. 예를 들어 버튼의 onClick에서 호출합니다. 트랜지션이 없으면 isPending이 올바르게 업데이트되지 않고, React가 개발 환경에서 오류를 기록합니다.
제출한 뒤 폼이 비워지는 이유는 무엇인가요?
폼 액션이 끝나면 React는 브라우저가 일반 제출 후에 하는 것처럼 폼의 비제어 필드를 초기화합니다. 오류 후에도 값을 유지하려면 state에 담아 반환하고 필드의 defaultValue에 넘기세요.