쿼리와 달리, 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
method 및 Query 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>
}
때때로 mutation request의 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>
)
}
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!
},
})
연속적인 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')
}
기본적으로 TanStack 쿼리는 오류 발생 시 mutation을 다시 시도하지 않지만, retry
옵션을 사용하면 가능합니다:
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
디바이스가 오프라인 상태여서 mutation이 실패하면 디바이스가 다시 연결될 때 동일한 순서로 mutation이 다시 시도됩니다.
필요한 경우 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()
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도 있습니다.
Mutations에 대한 자세한 내용은 커뮤니티 리소스에서 #12: React Mastering Mutations in React Query를 참조하세요.