onSuccess
,onError
andonSettled
have been removed from Queries. They haven't been touched for Mutations. Please see this RFC for motivations behind this change and what to do instead.
→ V5에서 onSuccess, onError, onSettled 가 useQuery 의 option 에서 사라졌다.
APIs are consistent, and the callbacks are not.
You can (apparently) use them to execute side effects, like showing error notifications:
아래와 같은 useQuery가 있을 때 에러 처리를 하기 위해서 onError에 callbackFn 을 등록할 수 있었다.
export function useTodos() {
return useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
onError: (error) => {
toast.error(error.message)
},
})
}
만약 onError가 없다면 useEffect 로 처리를 해야 한다. 근데 useTodos 를 쓰는 곳이 여러 군데라면 useTodo가 호출될 때마다(혹은 최신화될 때마다) useEffect 가 호출되고 toast 가 여러 번 뜨게 된다.
export function useTodos() {
const query = useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
})
React.useEffect(() => {
if (query.error) {
toast.error(query.error.message)
}
}, [query.error])
return query
}
onError 을 사용하면, useQuery가 같은 api call 을 하나로 묶어주니까 에러 콜백도 한 번만 일어나게 될 것이라고 예상할 수 있을 것이다. → 근데 이는 완벽히 잘못된 생각
onError는 각 인스턴스마다 실행되므로 useEffect 를 사용하는 에러 처리와 동일한 결과가 나타나게 된다. 즉, 원치 않게 에러 처리 함수를 여러 번 호출하게 될 수 있다.
이를 해결하기 위해서는 onError 가 아니라 전역인 QueryClient 에서 처리하는 것을 권장하고 있다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
toast.error(`Something went wrong: ${error.message}`),
}),
})
useState 로 state를 만들고, useQuery 가 호출되면 onSuccess 로 state를 동기화 시켜주는 패턴도 많이 보인다. 다음의 예시도 todo 데이터를 fetching 해온 이후에 onSuccess에서 todoCount 의 상태를 동기화해주고 있다. 근데 이렇게 쓰는 것은 안티 패턴이다.
export function useTodos() {
const [todoCount, setTodoCount] = React.useState(0)
const { data: todos } = useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
//😭 please don't
onSuccess: (data) => {
setTodoCount(data.length)
},
})
return { todos, todoCount }
}
만약, fetchTodos를 호출한 결과값의 length가 5라고 해보자. 그러면 데이터를 가져오고 렌더링하는 일련의 과정 중에는 다음의 3가지 상태가 존재하게 된다.
setTodoCount의 결과가 동기적으로 동작하지 않기 때문에 싱크가 안 맞는 시점이 존재하게 된다.
export function useTodos() {
const { data: todos } = useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
})
const todoCount = todos?.length ?? 0
return { todos, todoCount }
}
해결방법은 그냥 state를 안 쓰면 된다. data가 변경될 때마다 useTodos 가 계속 재실행될 것이므로 굳이 state를 필요가 없는 것이다.
싱크가 안 맞는 순간이 생기는 경우에 생길 잠재적 오류의 가능성이 있기 때문에 쓰지 말라고 tkdodo가 여러 번 얘기한 것 같지만.. 아무래도 API 가 존재하기 때문에 쓰지 말라고 해도 계속 비슷한 사례가 발생하는 것 같다.
redux 등 다른 store 를 추가로 사용하는 경우에 onSuccess 에서 싱크를 맞추고 싶을 수도 있다.
export function useTodos(filters) {
const { dispatch } = useDispatch()
return useQuery({
queryKey: ['todos', 'list', { filters }],
queryFn: () => fetchTodos(filters),
staleTime: 2 * 60 * 1000,
onSuccess: (data) => {
dispatch(setTodos(data))
},
})
}
하지만 만약 cache값이 남아있어서 fetch 를 하지 않았지만 data의 값이 바뀌었다면? onSuccess 를 호출하지 않으므로 useQuery의 반환값과 타 스토어에서 가지고 있는 값이 싱크가 안 맞게 된다.
export function useTodos(filters) {
const { dispatch } = useDispatch()
const query = useQuery({
queryKey: ['todos', 'list', { filters }],
queryFn: () => fetchTodos(filters),
staleTime: 2 * 60 * 1000,
})
React.useEffect(() => {
if (query.data) {
dispatch(setTodos(query.data))
}
}, [query.data])
return query
}
data가 바뀔 때마다 스토어의 값을 업데이트 해주어야 하므로 useEffect 에서 처리하는 것이 옳다고 한다.
물론 좋게 사용한 예시도 있긴 했지만 극히 드물고 위의 anti-pattern 을 만들 위험이 커서 없앴다고 한다.
이미 공식문서에 enabled 속성을 사용해서 구현하라고 나와있다.
Dependent Queries | TanStack Query Docs
RFC: remove callbacks from useQuery · TanStack/query · Discussion #5279
많은 도움이 되었습니다, 감사합니다.