
아래 글은 tanstack query 공식 문서(5.80.7)를 기반으로 쓰여졌습니다. 대부분의 예제와 설명은 tanstack query 문서에서 가져왔습니다.
useMutation쿼리와 다르게, mutation 은 데이터 변경 작업이나 서버 사이드 작업을 할 수 있다. Tanstack Query 는 이를 위해 useMutation 훅을 제공한다.
function Todo() {
const { isPending, isError, isSuccess, error, mutate } = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{isPending ? (
'Adding todo...'
) : (
<>
{isError ? (
<div>An error occurred: {error.message}</div>
) : null}
{isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
대표적으로 isPending, isError, isSuccess 를 통해 작업의 상태를 확인할 수 있고, error 로 오류를 받아올 수 있다.
mutate 를 통해 mutation 함수에 변수를 전달해 호출할 수 있다.
// 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>
}
mutate함수는 비동기 함수이기 때문에, React 16 과 그 이전 버전에서는 직접적으로 사용할 수 없다.
위의 예제에서 React 16 이전 버전에서는 new FormData() 라는 객체에 event.target 의 값을 저장하여 사용한다.
이는 React 의 Event Pooling 과 관련이 있다. React 16 까지는 Event Pooling 을 통해
SyntheticEvent를 재사용해서onSubmit과 같은 함수에서 event 객체를 비동기로 받아오기 위해서는event.persist()를 사용해서 React 가 Event Object 의 프로퍼티를 유지하도록 해야한다.
어떤 상황에서는 mutation 요청의 error 나 data 를 초기화해야 할 수도 있다. 초기화를 위해서는 reset 함수를 사용할 수 있다.
const CreateTodo = () => {
const [title, setTitle] = useState('')
const { mutate, error, reset, error } = useMutation({ mutationFn: createTodo })
const onCreateTodo = (e) => {
e.preventDefault()
mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{error && (
<h5 onClick={() => reset()}>{error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Todo</button>
</form>
)
}
useMutation 에서 제공하는 옵션을 사용해서 mutation 중 side effect 를 간편하게 발생시킬 수 있다.
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!
},
})
onMutate: mutate 가 발생했을 때 호출된다.onError: 에러가 발생했을 때 호출된다.onSuccess: 성공했을 때 호출된다.onSettled: 항상 호출된다.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!
},
})
mutate 함수에서도 side effect 를 발생시킬 수 있다. 다만 useMutation 보다 이후에 처리된다.
onSuccess, onError, onSettled callback 은 연속된 mutation 에서는 처리가 살짝 달라진다. mutate 함수에서는 컴포넌트가 여전히 마운트될때에 한번만 실행된다.
이유는 mutation observer 가 mutate 함수가 호출될 때마다 매번 삭제되고 구독 취소되기 때문이다. 이와 반대로 useMutation 은 mutate 호출마다 실행된다.
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// Will be called 3 times
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
성공 시 resolve 되고 실패 시 error 를 던지는 Promise 를 얻기 위해서는, mutate 가 아닌 mutateAsync 을 사용해야 한다.
const mutation = useMutation({ mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Tanstack Query 는 기본적으로 mutation 에서 에러가 발생했을 때 재시도를 하지 않는다. 하지만 retry 옵션을 사용한다면 가능하다.
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
Mutation 을 필요하다면 스토리지에 저장하고 나중에 재개할 수 있다. 이는 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()
dehydrate
dehydrate 는 HydrationBoundary 나 hydrate 를 통해 hydrated 될 수 있는 cache 의 고정된 형태를 저장한다. 기본적으로 성공한 쿼리만 포함하며, 서버에서 클라이언트로 prefetch 된 쿼리를 전달하거나 쿼리를 로컬 스토리지나 다른 장소에 저장할 때 유용하다.
import { dehydrate } from '@tanstack/react-query'
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery,
shouldDehydrateMutation,
})
어떤 스토리지 시스템에서는 JSON 직렬화가 가능한 값들을 요구할 수 있다.
Error,undefined같은 JSON 으로 자동 직렬화가 되지 않는 값들을 dehydrate 해야 한다면 직렬화를 직접 해야 한다.기본적으로 성공적인 쿼리만 포함하므로,
Error를 포함하고 싶다면shouldDehydrateQuery를 설정해야 한다.
// server
const state = dehydrate(client, { shouldDehydrateQuery: () => true }) // to also include Errors
const serializedState = mySerialize(state) // transform Error instances to objects
// client
const state = myDeserialize(serializedState) // transform objects back to Error instances
hydrate(client, state)
hydrate
hydrate 는 이전에 dehydrate 된 상태를 cache 로 추가한다.
import { hydrate } from '@tanstack/react-query'
hydrate(queryClient, dehydratedState, options)
hydration 을 하려는 쿼리가 이미 쿼리 캐시에 있다면,
hydrate는 데이터가 캐시에 있는 데이터보다 최신이여야만 데이터를 덮어쓴다.
HydrationBoundary
HydrationBoundary 는 이전에 dehydrate 된 상태를 useQueryClient() 로 반환될 수 있는 queryClient 에 추가한다. 클라이언트에 이미 데이터가 있는 경우엔 timestamp 를 기반으로 최신의 정보로 변경한다.
import { HydrationBoundary } from '@tanstack/react-query'
function App() {
return <HydrationBoundary state={dehydratedState}>...</HydrationBoundary>
}
쿼리들만
HydrationBoundary를 통해 dehydrate 될 수 있다.
기본적으로 모든 mutation 은 병렬로 돌아간다. 같은 mutation 의 mutate() 함수를 여러번 호출해도 말이다. 이것을 방지하기 위해서는 id 를 가지는 scope 를 mutation 에 넣으면 된다.
모든 mutation 은 같은 scope.id 를 가지고 있으면 직렬로 실행된다. 이것은 해당 범위 내에 이미 진행중인 mutation 이 있는 경우 isPaused: true 에서 시작된다는 것을 의미한다.
mutation 은 대기열에 추가되고 대기열에서 시간이 끝나면 다시 시작된다.
const mutation = useMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
공식 문서에서 아래 파트의 내용을 포함하고 있는 글입니다.