React 19 Concurrent 훅 - useTransition, useOptimistic vs React Query

useTransition

const [isPending, startTransition] = useTransition()

function handleFilter(value: string) {
  startTransition(async () => {
    const data = await fetchData(value)
    startTransition(() => setResults(data)) // await 뒤 set 은 다시 감싼다
  })
}
  • startTransition으로 감싼 state 업데이트는 **비긴급(non-urgent)**으로 처리
  • 급한 업데이트(타이핑, 클릭 피드백)를 먼저 처리하고, transition 작업은 뒤로 미룸
  • isPending으로 로딩 상태 확인, 기존 UI 유지하면서 백그라운드에서 새 UI 준비
  • await 뒤의 set 은 transition 으로 안 잡힌다 — 한 번 더 startTransition 으로 감싼다1

useOptimistic

const [optimisticItems, addOptimistic] = useOptimistic(
  items,
  (current, newItem) => [...current, newItem]
)

function handleAdd(item: Item) {
  startTransition(async () => {
    addOptimistic(item) // 즉시 UI 반영
    await saveToServer(item) // Action 이 끝나면 items 로 돌아간다
  })
}
  • 서버 응답 전에 UI 먼저 업데이트, 실패 시 자동 복구 — 낙관적 값은 Action 이 진행 중일 때만 보이고 끝나면 items 가 보인다. 그래서 성공하면 items 를 갱신해야 남는다2
  • setter 는 Action(startTransition·form action) 안에서 불러야 한다. 밖에서 부르면 경고와 함께 바로 되돌아간다3
  • 좋아요 버튼, 장바구니 추가 같은 인터랙션에 적합

현실: Query가 이미 너무 편함

const { data, isPending } = useQuery({
  queryKey: ['items', filter],
  queryFn: () => fetchItems(filter),
})

useMutation({
  mutationFn: addItem,
  onMutate: async (newItem) => {
    const previous = queryClient.getQueryData(['items'])
    queryClient.setQueryData(['items'], (old) => [...old, newItem])
    return { previous }
  },
  onError: (err, _, context) => {
    queryClient.setQueryData(['items'], context.previous)
  },
})

캐싱, 리페치, devtools, stale-while-revalidate까지 한 방에 해결. 팀에서 이미 쓰고 있으면 “굳이?” 됨.

useTransition이 의미 있는 지점 - 무거운 클라이언트 연산, Next.js Server Actions 조합

<form action={(formData) => {
  startTransition(async () => {
    await createItem(formData)
    router.refresh()
  })
}}>

Footnotes

  1. useTransition – React: “Any async calls that are awaited in the action will be included in the Transition, but currently require wrapping any set functions after the await in an additional startTransition …” ↩

  2. useOptimistic – React: “Optimistic state only renders while an Action is in progress, otherwise value is rendered.” ↩

  3. useOptimistic – React: “When you call the setter outside an Action, the optimistic state will briefly appear and then immediately revert back to the original value.” ↩

`#495`