Guides&Concepts_(Mutation)
Mutation
쿼리들(useQuery, useQueries...etc)과는 다르게 'useMutation'은 주로 create/update/delete 혹은 서버쪽 작업을 수행하는 역할을 한다. 아래에는 간단한 todo를 서버에 추가하는 예시코드가 있다.
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' 혹은 'status === idle' - mutation이 놀고있거나, fresh/reset상태에 있는 것을 의미한다.
(단, mutation의 쿼리함수가 실행되고나면 'isLoading', 'isSucess', 'isError'상태중 하나가 된다. 즉, isIdle상태는 쿼리함수가 실행되지 않은 상태라는 것을 반증한다.) - 'isLoading' 혹은 'status === loading' - mutation이 작동중이라는 것을 의미한다.
- 'isError' 혹은 'status === error' - mutation이 실패했음을 의미한다.
- 'isSucess' 혹은 'status === sucess' - mutation이 성공적으로 작업을 마쳤고, mutation의 data를 사용가능함을 의미한다.
위의 주요한 상태를 넘어서 각각의 state에서 사용(이용)할 수 있는 정보들이 있다.
- 'error' - 만약 mutation이 'error'상태라면 'error' 프로퍼티를 이용할 수 있다. (주로 에러메시지 표시할때 이용)
- 'data' - 만약 mutation이 'sucess'상태라면 'data'프로퍼티를 이용할 수 있다.
제공된 예시코드에서 위의 state, property말고도 mutate함수를 이용해 muation함수에 무언가를 전달하는 것을 보았을 것이다.
전달되는 무언가는 단일변수 혹은 객체도 될 수 있다. (단일변수 혹은 객체만 가능하다.)
다양한 옵션들, 예를들어 'onSucess'옵션, 쿼리클라이언트의 'invalidateQueries'메서드, 쿼리클라이언트의 'setQueryData'메서드같은 것을을 이용하면 mutation을 더 폭넓게 활용할 수 있다.
Important: 'mutate'함수는 비동기함수이다. 따라서 'mutate'함수는 React 16버전아래에서 event callback으로 직접적사용이 불가하다. 만약 'onSubmit'같은 event에 덧붙여 사용하려면 '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의 결과로 생긴 'error' 혹은 'data'프로퍼티를 지우고싶다면 '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의 생애주기동안 유용하게 사용되는 side-effects옵션을 제공한다.
이 옵션들은 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'을 호출할때 정의된 콜백 이외의도 추가적인 콜백을 실행시키고자 할 때, 'muatate'함수를 동일한 콜백옵션(onSuccess, onError, 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!
},
})
Consecutive mutations
'onSuccess', 'onError', 'onSettled' 콜백을 다룰때에는 약간의 차이점이 있다. 'mutate'함수에 전달되었을 때, 이 콜백들은 컴포넌트가 마운트된 상태에서만 단 한번 호출된다. 이는 'mutate'함수가 호출될 때마다 mutation observer가 삭제 -> 재등록되기 때문이다.
반면 'useMutation'핸들러는 각 'mutate'호출이 일어날때마다 실행된다. 이는 'mutate'함수에 전달된 콜백들과 달리, 여러 번 호출될 수 있으며 컴포넌트가 마운트된 상태와 관계없이 실행될 수 있다는 것을 의미한다.
'useMutation'에 전달된 'mutation'함수는 대부분 비돌기적으로 처리된다. 이 경우 mutate함수 호출 순서와 완료 순서가 다를 수 있다. 이는 mutate함수가 비동기 함수인 경우 발생하며, 비동기 함수는 다른 함수보다 먼저 완료될 수 있기 때문이다. 따라서 mutate함수 호출 순서와 관계없이 각 mutation요청의 완료 순서를 처리해야 한다.
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
'mutateAsync'를 사용하면 성공 시 resolve되거나 오류 시 error를 던지는 프로미스를 직접적으로 얻을 수 있다.
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 Query는 실패한 mutation에 대해 재시도를 하지 않는다. 만약 재시도를 원한다면 'retry'옵션을 이용하면 된다.
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
Persist mutation
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()
Persisting Offline mutations
If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function.
This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found.
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>
)
}
We also have an extensive offline example that covers both queries and mutations.
Further reading
For more information about mutations, have a look at #12: Mastering Mutations in React Query from the Community Resources.