본문 바로가기

카테고리 없음

[번역] Optimistic Updates

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

 

Optimistic Updates | TanStack Query Docs

 

tanstack.com

 

React Query는 mutation이 완료되기 전에 UI를 낙관적으로 업데이트하는 두가지 방법을 제공한다. onMutate 옵션을 사용하여 캐시를 직접 업데이트하거나, 반환된 변수를 활용하여 useMutation 결과에서 UI를 업데이트할 수 있다. 

 

Via the UI

이것은 캐시와 직접 상호작용하지 않기 때문에 더 간단한 변형이다. 

 

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'] })
  },
})

 

당신은 이제 addTodoMutation.variables 변수에 엑세스할 수 있다. UI 목록에서 mutation이 pending 중인 동안 목록에 다른 항목을 추가할 수 있다. 

 

<ul>
  {todoQuery.items.map((todo) => (
    <li key={todo.id}>{todo.text}</li>
  ))}
  {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>

 

mutation이 Pending 중인동안에는 다른 불투명도로 임시 아이템을 렌더링할 수 있다. mutation이 완료되면 해당 항목은 자동으로 더 이상 렌더링되지 않는다. 리페치가 성공했으므로 목록에서 해당 아이템이 '정상 아이템'으로 표시된다. 

 

mutation 오류가 발생하면 항목도 사라진다. 그러나 원하는 경우 mutation의 isError 상태를 확인하여 계속 표시할 수 있다. 변수는 mutation오류가 발생해도 지워지지 않으므로 계속 엑세스 할 수 있으며 재시도 버튼을 표시할 수도 있다.  

 

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

 

If the mutation and the query don't live in the same component

이 접근 방식은 mutation과 쿼리가 동일한 컴포넌트에 있는 경우 매우 잘 작동하지만, 전용 useMutationState 훅을 통해 다른 컴포넌트의 모든 mutation에도 접근할 수 있다. mutation키와 함께 사용하는 것이 가장 좋다. 

 

// 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,
})

 

variables는 동시에 여러개의 mutation이 될 수 있으므로 배열이된다. 항목에 대한 고유 키가 필요한 경우 mutation.state.submittedAt을 선택할 수도 있다. 이렇게하면 낙관적인 업데이트를 동시에 표시하는 것도 쉬워진다. 

 

Via the cache

mutation을 수행하기 전에 상태를 낙관적으로 업데이트하면 mutation이 실패할 가능성이 있다. 이러한 실패 사례의 대부분은 낙관적인 쿼리에 대해 리페치를 트리거하여 실제 서버 상태로 되돌릴 수 있다. 하지만 일부 상황에서는 리페칭조자 제대로 작동하지 않을 수 있으며, mutation 오류는 refetching할 수 없는 서버 문제를 나타낼 수 있다. 이 경우 대신 업데이트를 롤백하도록 선택할 수 있다. 

 

이를 위해 useMutation의 onMutate 핸들러 옵션을 사용하면 나중에 onError 및 onSettled 핸들러에 모두 전달할 값을 마지막 인수로 반환할 수 있다. 대부분의 경우 롤백 함수를 전달하는 것이 가장 유용하다. 

 

updating a list of todos when adding a new todo 

const queryClient = useQueryClient()

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'] })
  },
})

 

 

updating a single todo 

 

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', newTodo.id] })

    // Snapshot the previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

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

    // Return a context with the previous and new todo
    return { previousTodo, newTodo }
  },
  // If the mutation fails, use the context we returned above
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // Always refetch after error or success:
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})

 

원하는 경우 별도의 onError 및 onSuccess 핸들러 대신 onSettled 함수를 사용할 수도 있다. 

 

useMutation({
  mutationFn: updateTodo,
  // ...
  onSettled: (newTodo, error, variables, context) => {
    if (error) {
      // do something
    }
  },
})

 

When to use what

낙관적인 결과를 표시해야하는 곳이 한 곳만 있는 경우 변수를 사용하고 UI를 직접 업데이트하는 것이 코드가 덜 필요하고 일반적으로 추론하기 쉬운 접근 방식이다. 예를 들어 롤백을 전혀 처리할 필요가 없다. 

 

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