React 19에서는 <form action={함수}>에 함수를 바로 넘길 수 있고, useActionState가 그 액션의 대기 상태와 마지막 결과를 한 훅으로 관리한다. useState + onSubmit + 로딩 플래그로 짜던 패턴을 대체한다.
'use client'
const [state, formAction, isPending] = useActionState(updateName, null)
// updateName(prevState, formData) — 서버 함수여도 된다
<form action={formAction}>
<input name="name" disabled={isPending} />
<button disabled={isPending}>저장</button>
{state?.error && <p>{state.error}</p>}
</form>- 액션은 이전 상태와
FormData를 받아 다음 상태(에러 메시지, 결과)를 반환한다. 서버 쪽 검증 결과를 그대로 화면에 보여 주기 좋다(서버 함수와 서버 액션) - form action으로 넘긴 함수는 자동으로 transition 안에서 실행된다(useTransition)
- 점진적 향상(Progressive Enhancement): 서버 액션과 함께 쓰면 JS(JavaScript)가 로드되기 전에도 폼이 제출되고, 하이드레이션 전 제출은 나중에 다시 실행된다(하이드레이션)
useFormStatus: 폼 안쪽의 자식 컴포넌트(제출 버튼)가 props를 받지 않고도 상위 폼의 제출 상태를 읽는다useOptimistic: 서버 확정 전에 결과를 미리 화면에 반영한다. 아래 절을 본다
입력값을 state로 들지 않는 비제어 입력과 잘 맞는다. 같은 일을 클라이언트 캐시 쪽에서 하는 방법은 TanStack Query의 useMutation이다.
useOptimistic
useOptimistic(value, reducer?)는 [optimisticState, setOptimistic]을 돌려준다. Action이 진행 중일 때만 낙관적 값이 렌더되고, 진행 중인 Action이 없으면 value가 그대로 렌더된다.
// todos: 부모가 저장 성공 후 갱신하는 실제 목록
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }] // reducer
)
function handleAdd(text: string) {
const newTodo = { id: crypto.randomUUID(), text }
startTransition(async () => {
addOptimisticTodo(newTodo) // pending 항목이 바로 보인다
await addTodoAction(newTodo) // 성공하면 부모가 todos를 갱신한다
})
}- reducer는 순수 함수여야 한다. 저장 중에
todos가 바뀌어도 최신 목록 위에 임시 항목을 얹어 준다 - setter는 Action이나
startTransition안에서 부른다. 밖에서 부르면 경고가 뜨고 낙관적 값이 잠깐만 보인다. form action처럼 Action으로 넘긴 함수는 이미 transition 안이라 그대로 불러도 된다 - 따로 "되돌리는" 코드는 없다. Transition이 끝나면 그 시점의
value가 렌더된다. 성공하면 부모가 갱신한 실제 값과 같은 렌더에서 합쳐지고, 실패하면value가 그대로라서 원래 화면으로 돌아간다
출처: React — useOptimistic · React — 목록에 낙관적으로 추가하기 · React — setOptimistic 주의 사항