공식문서 : https://tanstack.com/query/v4/docs/guides/mutations
쿼리와 달리, mutation은 일반적으로 데이터를 생성/업데이트/삭제하거나 서버 side-effect를 수행하는데 사용된다. 이를 위해, React Query는 useMutation
hook를 내보낸다.
다음은 서버에 새로운 todo를 추가하는 mutation의 예이다 :
function App() {
const mutation = useMutation(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은 주어진 순간에 다음과 같은 state 중 하나에만 있을 수 있다.
isIdle
또는 status === 'idle'
- mutation이 현재 idle이거나 fresh/reset state이다.isLoading
또는 status === 'loading'
- mutation이 현재 runningisError
또는 status === 'error'
- mutation에 error 발생isSuccess
또는 status === 'success'
- mutation이 성공했으며 mutation data를 사용할 수 있다.이러한 기본 state들 외에도, mutation의 state에 따라 더 많은 정보를 사용할 수 있다.
error
- mutation이 error
state에 있는 경우 error
속성을 통해 error를 사용할 수 있다.data
- mutation이 success
state에 있는 경우 data
속성을 통해 data를 사용할 수 있다.위의 예제에서, 단일 변수 또는 객체로 mutate 함수를 호출하여 mutation function에 변수를 전달할 수도 있다.
변수만 있어도, mutation이 그렇게 특별한건 아니지만, onSuccess
옵션, Query Client의 invalidateQueries
메서드 및 Query Client의 setQueryData
메서드와 함께 사용하면 mutation은 매우 강력한 도구가 된다.
중요 : mutation 함수는 비동기 함수이므로, React 16 및 이전 버전의 event callback에서 직접 사용할 수 없다.
onSubmit
시 event에 접근해야하는 경우 다른 함수로 mutate를 감싸야한다. 이는 React의 event pooling 때문이다.
// This will not work in React 16 and earlier
const CreateTodo = () => {
const mutation = useMutation(event => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
})
return <form onSubmit={mutation.mutate}>...</form>
}
// This will work
const CreateTodo = () => {
const mutation = useMutation(formData => {
return fetch('/api', formData)
})
const onSubmit = event => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
mutation 요청의 data
나 error
를 지워야하는 경우가 있다. reset
함수를 사용하여 이를 처리할 수 있다 :
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation(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>
)
}
useMutation
은 mutation의 lifecycle동안 모든 단계에서 빠르고 쉬운 side-effect를 허용하는 helper 옵션과 함께 제공된다. 이들은 mutation 이후 쿼리를 무효화하고 refetch하는데 유용하며 심지어는 optimistic update에도 유용하다.
useMutation(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!
},
})
콜백함수에서 promise를 반환할 때 다음 콜백이 호출되기 전에 먼저 기다린다 :
useMutation(addTodo, {
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
})
mutate를 호출할때 useMutation
에 정의된 콜백 이외의 추가 콜백들을 트리거하고 싶을 수 있다. 이는 component-specific side effect들을 트리거하는데 사용할 수 있다. 그렇게 하기 위해 mutation 변수 뒤에 있는 mutate
function에 동일한 콜백 옵션을 제공할 수 있다. 지원되는 재정의에는 : onSuccess
, onError
및 onSettled
가 있다. mutation이 완료되기 전에 컴포넌트가 unmount되면 추가 콜백이 실행되지 않음을 명심하라.
useMutation(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!
},
})
연속적인 mutation의 경우 onSuccess
, onError
및 onSettled
콜백에 대한 처리는 약간의 차이가 있다. mutate
함수에 전달되면, 컴포넌트가 여전히 mount된 경우에만 단 한번만 실행된다. 이는 mutate
함수가 호출될 때마다 mutation observer가 제거되고 resubscribe되기 때문이다. 반대로, useMutation
핸들러는 각 mutate
호출에 대해 실행된다.
useMutation
에 전달되는mutationFn
은 대부분 비동기이다. 이 경우 mutation들이 실행되는 순서는mutate
function 호출의 순서와 다를 수 있다.
useMutation(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
},
})
})
success시 resolve하거나 error를 throw하는 promise를 얻기 위해서는 mutate
대신 mutateAsync
를 사용해라. 이는 예시로 side effect들을 구성하는데 사용할 수 있다.
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
기본적으로 React Query는 error시 mutation을 retry하지 않지만, retry
옵션을 사용하면 가능하다 :
const mutation = useMutation(addTodo, {
retry: 3,
})
디바이스가 오프라인이라 mutation이 실패하면 디바이스가 재연결될때와 동일한 순서로 retry된다.
mutation들은 필요한 경우 storage에 유지되고 나중에 재시작할 수 있다. 이는 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(['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(['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()
만약 persistQueryClient 플러그인을 사용하여 오프라인 mutation을 지속한다면, 기본 mutation 함수를 제공하지 않는 한 페이지가 재로드될때 mutation을 재개할 수 없다.
이는 기술적인 한계이다. 외부 스토리지에 유지하는 경우, 함수들을 직렬화할 수 없으므로 mutation의 state만 지속된다. hydration 이후, mutation을 트리거하는 컴포넌트가 mount되지 않을 수 있으므로, resumePausedMutations
을 호출하면 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.upateTodo(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을 모두 다루는 광범위한 오프라인 예제가 있다.
mutation에 대한 자세한 내용은 Community Resources에서 제공하는 #12:Mastering Mutations in React Query를 참고하라.