[React-Query] Optimistic Updates

강동욱·2024년 2월 16일
0

Optimistic Updates

들어가기 앞서

Optimistic Updates를 할 수 있는 방법은 2가지가 있다.

  • onMutate 옵션을 사용해 캐시를 직접 업데이트
  • useMutation훅의 반환값인 variables를 이용해서 Optimistic UI 구성

UI와 상호작용

useMutation variables

이 방법은 캐시와 직접적으로 상호작용하지는 않는다.

const { isPending, submittedAt, variables, mutate, isError } = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // make sure to _return_ the Promise from the query invalidation
  // so that the mutation stays in `pending` state until the refetch is finished
  onSettled: async () => {
    return await queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
<ul>
  {todoQuery.items.map((todo) => (
    <li key={todo.id}>{todo.text}</li>
  ))}
  {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>

위의 예제에서 addTodoMutation.variables 사용하면 쿼리가 렌더링 될 때 mutation이 pending일 때 추가하려는 todo 아이템을 opacity를 다르게 설정해서 구분한 뒤 미리 보여줄 수 있다. refetch가 성공되면 isPending false가 되고 추가하려는 todoItem은 기존의 아이템과 똑같은 opacity를 가지면서 화면에 렌더링 된다.

만약 mutation에 에러가 생기면 isError 상태를 활용해 확인할 수 있다. 하지만 variabels 자체만으로는 명확하지 않음으로 에러가 발생했을 때 버튼 같은 것을 보여주면서 다시 mutate를 다뤄줄 수 있게 해야한다.

 isError && (
    <li style={{ color: 'red' }}>
      {variables}
      <button onClick={() => mutate(variables)}>Retry</button>
    </li>
  )

useMutationState

// somewhere in your app
const { mutate } = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
})

// access variables somewhere else
const variables = useMutationState<string>({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
})

useMutationState훅을 활용해 mutationKey 같이 사용하여 다른 컴포넌트에서 mutation을 실행할 수 있다. useMutationState훅을 사용한 varaiables는 배열행태로 전달되어진다. 왜냐하면 다수의 mutation 동시에 실행될 수 있기 때문이다.

Cache와 상호작용

우리가 mutation하기 전에 우리의 상태를 낙관적으로 업데이트 할 때 mutation이 실패해 에러가 발생할 가능성이 있다. 이런 에러상황이 생기면 보통은 optimistic 쿼리를 재요청해 서버 상태가 true가 되게 한다. 일부 상황에서는 optimistic 쿼리를 재요청해도 제대로 동작하지 않을 수 있고 재요청을 할 수 없게 만드는 서버 문제 때문에 mutation에러가 발생할 수 있다. 이런 경우 우리는 롤백을 사용해 업데이트를 할 수 있다.

useMutation({
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Snapshot the previous value
    const previousTodos = queryClient.getQueryData(['todos'])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // Return a context object with the snapshotted value
    return { previousTodos }
  },
  // If the mutation fails,
  // use the context returned from onMutate to roll back
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // Always refetch after error or success:
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
  • 중복요청을 방지하기 위해서 queryClient.cancelQueries를 사용한다.
  • previousTodos 변수에다가 이전에 사용했던 값을 확보해 놓는다
  • 낙관적 업데이트를 진행시키고 previousTodos값을 리턴한다.(리턴하는 이유는 롤백을 위해서이다.)
  • 에러가 발생했을 때는 onError콜백함수를 호출해 onMutate에서 반환한 context에서 previousTodos를 사용해 롤백 시킨다.
  • 마지막으로 queryClient.invalidateQueries해서 캐싱된 값을 refetch 한다.

언제는 Cache와 상호작용하고 언제는 UI와 상호작용하는지?

낙관적인 결과를 표시해야 할 위치가 하나뿐이라면, 변수를 사용하고 UI를 직접 업데이트하는 것이 코드를 덜 작성하고 일반적으로 이해하기 쉽다. 예를 들어, 롤백을 처리할 필요가 전혀 없디.

그러나 화면에 여러 위치에서 업데이트를 알아야 하는 경우, 캐시를 직접 조작하는 것이 자동으로 처리해준다.

출처

https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates

profile
차근차근 개발자

0개의 댓글