[TanStakQuery] Mutations

Jeris·2023년 5월 22일
0

쿼리와 달리, mutations는 일반적으로 데이터를 생성/업데이트/삭제하거나 서버 사이드 이펙트를 수행하는 데 사용됩니다. 이를 위해 TanStack Query는 useMutation 훅을 내보냅니다.

다음은 서버에 새 할 일을 추가하는 mutation의 예입니다:

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isLoading ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

Mutation은 주어진 순간에 다음 상태 중 하나에만 있을 수 있습니다:

  • isIdle 또는 state === 'idle' - mutation이 현재 idle 상태이거나 새로 고침/리셋 상태입니다.
  • isLoading 또는 state === 'loading' - mutation이 현재 실행 중입니다.
  • isError 또는 state === 'error' - mutation에서 오류가 발생했습니다.
  • isSuccess 또는 state === 'success' - mutation이 성공했으며 mutation 데이터를 사용할 수 있습니다.

이러한 기본 상태 외에도 mutation 상태에 따라 더 많은 정보를 사용할 수 있습니다:

  • error - mutation이 error 상태인 경우 error 프로퍼티를 통해 오류를 사용할 수 있습니다.
  • data - mutation이 success 상태인 경우 data 프로퍼티를 통해 데이터를 사용할 수 있습니다.

위의 예제에서는 단일 변수 또는 객체mutate 함수를 호출하여 변수를 mutationw 함수에 전달할 수 있음을 확인했습니다.

변수만 있으면 mutations는 그다지 특별하지 않지만, onSuccess 옵션, Query Client's invalidateQueries methodQuery Client's setQueryData method와 함께 사용하면 mutations가 매우 강력한 도구가 됩니다.

"중요: mutate 함수는 비동기 함수이므로 React 16 및 이전 버전에서는 이벤트 콜백에서 직접 사용할 수 없습니다. onSubmit에서 이벤트에 접근해야 하는 경우 다른 함수에서 mutate를 래핑해야 합니다. 이는 React event pooling 때문입니다."

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

Resetting Mutation State

때때로 mutation request의 errordata를 지워야 하는 경우가 있습니다. 이를 위해 reset 함수를 사용하여 처리할 수 있습니다:

const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const mutation = useMutation({ mutationFn: createTodo })

  const onCreateTodo = (e) => {
    e.preventDefault()
    mutation.mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

Mutation Side Effects

useMutation에는 mutation lifecycle 중 어느 단계에서든 빠르고 쉽게 side-effects를 처리할 수 있는 몇 가지 헬퍼 옵션이 제공됩니다. 이러한 옵션은 mutations 후 쿼리를 무효화하거나(invalidating) refetching할 때, 심지어 optimistic updates 할 때도 유용합니다.

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    // A mutation is about to happen!

    // Optionally return a context containing data to use when for example rolling back
    return { id: 1 }
  },
  onError: (error, variables, context) => {
    // An error happened!
    console.log(`rolling back optimistic update with id ${context.id}`)
  },
  onSuccess: (data, variables, context) => {
    // Boom baby!
  },
  onSettled: (data, error, variables, context) => {
    // Error or success... doesn't matter!
  },
})

콜백 함수에서 프로미스를 리턴하 때 다음 콜백이 호출되기 전에 먼저 대기합니다:

useMutation({
  mutationFn: addTodo,
  onSuccess: async () => {
    console.log("I'm first!")
  },
  onSettled: async () => {
    console.log("I'm second!")
  },
})

mutate를 호출할 때 useMutation에 정의된 콜백 외에 추가 콜백을 트리거하고 싶을 수 있습니다. 이는 컴포넌트별 side effects를 트리거하는 데 사용할 수 있습니다. 이렇게 하려면 mtation 변수 뒤에 동일한 콜백 옵션을 mutate 함수에 제공하면 됩니다. 지원되는 옵션으로는 onSuccess, onError, onSettled가 있습니다. 단, mutation이 완료되기 전에 컴포넌트가 마운트 해제되면 이러한 추가 콜백은 실행되지 않는다는 점에 유의하세요.

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, context) => {
    // I will fire first
  },
  onError: (error, variables, context) => {
    // I will fire first
  },
  onSettled: (data, error, variables, context) => {
    // I will fire first
  },
})

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // I will fire second!
  },
  onError: (error, variables, context) => {
    // I will fire second!
  },
  onSettled: (data, error, variables, context) => {
    // I will fire second!
  },
})

Consecutive mutations

연속적인 mutations 관련해서는 onSuccess, onError, onSettled 콜백 처리에 약간의 차이가 있습니다. mutate 함수에 전달되면 컴포넌트가 여전히 마운트되어 있는 경우에만 한 번만 실행됩니다. 이는 mutate 함수가 호출될 때마다 mutation observer가 제거되었다가 다시 구독되기 때문입니다. 반대로, useMutation 핸들러는 각 mutate 호출에 대해 실행됩니다.

"대부분의 경우 useMutation에 전달된 mutationFn은 비동기식이라는 점에 유의하세요. 이 경우 mutations가 수행되는 순서는 mutate 함수 호출 순서와 다를 수 있습니다."

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, error, variables, context) => {
    // Will be called 3 times
  },
})

[('Todo 1', 'Todo 2', 'Todo 3')].forEach((todo) => {
mutate(todo, {
onSuccess: (data, error, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})


### Promises
성공하거나 오류를 발생시키는 프로미스를 얻으려면 `mutate` 대신 `mutateAsync`를 사용하세요. 아래는 side effects를 구성하는 데 사용할 수 있는 예시입니다.
```tsx
const mutation = useMutation({ mutationFn: addTodo })

try {
  const todo = await mutation.mutateAsync(todo)
  console.log(todo)
} catch (error) {
  console.error(error)
} finally {
  console.log('done')
}

Retry

기본적으로 TanStack 쿼리는 오류 발생 시 mutation을 다시 시도하지 않지만, retry 옵션을 사용하면 가능합니다:

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})

디바이스가 오프라인 상태여서 mutation이 실패하면 디바이스가 다시 연결될 때 동일한 순서로 mutation이 다시 시도됩니다.

Persist mutations

필요한 경우 mutations를 저장소에 유지했다가 나중에 다시 시작할 수 있습니다. 이는 hydration 함수를 사용하여 수행할 수 있습니다:

const queryClient = new QueryClient()

// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
  mutationFn: addTodo,
  onMutate: async (variables) => {
    // Cancel current queries for the todos list
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Create optimistic todo
    const optimisticTodo = { id: uuid(), title: variables.title }

    // Add optimistic todo to todos list
    queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])

    // Return context with the optimistic todo
    return { optimisticTodo }
  },
  onSuccess: (result, variables, context) => {
    // Replace optimistic todo in the todos list with the result
    queryClient.setQueryData(['todos'], (old) =>
      old.map((todo) =>
        todo.id === context.optimisticTodo.id ? result : todo,
      ),
    )
  },
  onError: (error, variables, context) => {
    // Remove optimistic todo from the todos list
    queryClient.setQueryData(['todos'], (old) =>
      old.filter((todo) => todo.id !== context.optimisticTodo.id),
    )
  },
  retry: 3,
})

// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })

// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)

// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)

// Resume the paused mutations:
queryClient.resumePausedMutations()

Persisting Offline mutations

persistQueryClient plugin을 사용하여 오프라인 mutations를 유지하는 경우 default mutation 함수를 제공하지 않는 한 페이지를 다시 로드할 때 mutations를 다시 시작할 수 없습니다.

이는 기술적 제한 사항입니다. 외부 저장소에 지속적으로 연결될 때는 함수를 직렬화할 수 없으므로 mutations 상태만 지속됩니다. Hydration 후 mutation을 트리거하는 컴포넌트가 마운트되지 않을 수 있으므로 resumePausedMutations를 호출하면 오류가 발생할 수 있습니다: No mutationFn fount.

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
})

// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(['todos'], {
  mutationFn: ({ id, data }) => {
    return api.updateTodo(id, data)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations()
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  )
}

또한 쿼리와 mutation을 모두 다루는 광범위한 offline example도 있습니다.

Further reading

Mutations에 대한 자세한 내용은 커뮤니티 리소스에서 #12: React Mastering Mutations in React Query를 참조하세요.

profile
job's done

0개의 댓글