useMutation 은 useEffect, useCallback 같은 hook 과는 다르게 의존성을 설정할 수 없다. 그럼, 리렌더링 시에 useMutation에서 반환되는 mutate() 는 항상 새로 만들어 지는 걸까?
useMutation 에 파라메터로 넘기는 mutaionFn, onMutate, onSettled, onSuccess 등이 리렌더링 시 변경될 경우, mutate() 를 호출 하면 각 상황에서 변경된 함수가 호출되는 것으로 봐서 그럴 수도 있어 보인다.
그렇다면 mutate() 를 호출 해서 mutationFn 이 이미 실행 중인 상황에서 리렌더링이 일어나 onSuccess 등이 변경 되고, 그 후 mutationFn 의 실행이 완료된다면 어떻게 될까? mutationFn 이 성공적으로 수행된 후 호출 되는 onSuccess 는 앞서 mutate() 가 호출 되었을 당시의 onSuccess 함수가 아니라 리렌더링 중 변경된 onSuccess 함수가 호출된다.
실제 react-query 가 어떻게 구현되어 있는지 살펴보자.
const [observer] = React.useState(
() =>
new MutationObserver<TData, TError, TVariables, TContext>(
queryClient,
options,
),
)
useMutation 의 구현을 보면 observer 라는 state 를 만들어 쓰는 것을 볼 수 있다.
React.useEffect(() => {
observer.setOptions(options)
}, [observer, options])
useMutation 에 파라메터로 넘어가는 옵션이 변경될 경우, 이 observer state 의 setOptions() 를 호출 하여 변경해 주게되며,
const mutate = React.useCallback<
UseMutateFunction<TData, TError, TVariables, TContext>
>(
(variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
},
[observer],
)
우리가 useMutation() 의 결과로 반환받는 mutate 는 useCallback() 을 사용하여 만드는데, 의존성 목록에는 observer 만 들어가 있어 mutate 는 새로 만들어지지 않는다.
좀 더 안 쪽을 살펴 보자. observer 의 mutate() 를 호출 했을때, 즉, 우리가 useMutation()에서 반환 받은 mutate() 함수를 호출 했을 때 무슨일이 일어나는걸까?
mutate(
variables?: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>,
): Promise<TData> {
this.mutateOptions = options
if (this.currentMutation) {
this.currentMutation.removeObserver(this)
}
this.currentMutation = this.client.getMutationCache().build(this.client, {
...this.options,
variables:
typeof variables !== 'undefined' ? variables : this.options.variables,
})
this.currentMutation.addObserver(this)
return this.currentMutation.execute()
}
mutate() 가 호출 되는 시점에 현재의 옵션을 넘겨서 Mutation 객체를 새로 만들고, 마지막에 Mutation.execute() 함수를 호출해 준다.
async execute(): Promise<TData> {
const executeMutation = () => {
this.retryer = createRetryer({
fn: () => {
if (!this.options.mutationFn) {
return Promise.reject('No mutationFn found')
}
return this.options.mutationFn(this.state.variables!)
},
...
})
return this.retryer.promise
}
...
const data = await executeMutation()
// Notify cache callback
await this.mutationCache.config.onSuccess?.(
data,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)
...
}
Mutation.execute() 는 생성 시 넘겨 받았던 options의 mutationFn 을 호출 하고, 그 결과에 따라 onSuccess 등을 호출 해 준다.
여기까지 봤을때는, mutate() 호출 시 객체를 생성하고, 필요한 정보는 해당 객체가 다 가지고 있는 형태이므로 이후, useMutation() 이 새로 호출되고 옵션이 바뀌더라도, 현재 실행중인 mutate 에는 영향을 미치지 않을 것으로 보인다.
그런데 아직 보지 못한 곳이 있다. observer의 setOptons().
setOptions(
options?: MutationObserverOptions<TData, TError, TVariables, TContext>,
) {
const prevOptions = this.options
this.options = this.client.defaultMutationOptions(options)
if (!shallowEqualObjects(prevOptions, this.options)) {
this.client.getMutationCache().notify({
type: 'observerOptionsUpdated',
mutation: this.currentMutation,
observer: this,
})
}
this.currentMutation?.setOptions(this.options)
}
뭔가 이유가 있겠지만, 개인적으로는 좀 의아한데… 마지막에 현재 mutation, 즉, 마지막으로 mutate() 호출될 당시의 mutation 객체의 옵션을 같이 바꿔준다.
이로인해, mutaionFn 이 실행 중 useMutation의 옵션이 바뀌게 되면, mutationFn 이 끝난 후 처리는 바뀐 옵션대로 하게되는 것이다.
여기서 useMutation() 사용할때 한 가지 주의 사항이 생긴다. mutate()를 호출한 후 useMutation()의 옵션이 변하는 경우에 대해 고려하라는 것.
의도한게 아니라면, mutate() 호출이 완료되기 전에 useMutation() 의 옵션이 변경된는 상황은 막아야 한다. 보통 react-query 를 사용하는 목적을 생각해보면, mutationFn 을 API 호출 작업일 것이고, 해당 작업은 서버나 네트워크 상황 등에 따라 매우 느리게 끝날 가능성이 있다. 그리고, 그 사이 useMutation()의 옵션에 영향을 주는 state 가 변경되어 리렌더링이 일어나면, mutationFn 이 완료되었을 때는 의도치 않은 작업이 수행될 수 있다.