지난 번에는 useMutation
의 기본 적인 사용 방법에 대해서 소개했다. 이번에는 전 편에서 잠깐 언급했던 custom hook을 사용하면서 queryClient랑 함께 사용하기와 optimistic updates
에 대한 내용을 적으려한다.
useMutation
도 useQuery
와 마찬가지로 커스텀 훅으로 만들어서 사용하면 많이 유용하다.
onSuccess
와 onError
를 사용했을 때의 경우이다.// src/hooks/
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
export default function useAddTodoMutation(): UseMutationResult<TodoType, AxiosError, TodoType> {
return useMutation(addTodo, {
onSuccess: (data) => {
console.log(data); // mutation 이 성공하면 response를 받을 수 있다.
},
onError: (error) => { // mutation 이 에러가 났을 경우 error를 받을 수 있다.
console.error(error);
},
});
}
(1)번의 훅에는 단점이 있다.
공통으로 사용되는 훅인데, mutation 이 실행되고 난 후의 행동이 각각 다르다면?? onSuccess 등의 추가 콜백을 달기엔 애매한 상황이 온다.
그럴 땐 useAddTodoMutation 에 달았던 콜백 함수를 지우고, 아래 예시처럼 mutate 옵션 자리에 작성해주면 된다.
(전 편에서 언급했다시피 mutate 의 추가 콜백은 useMutation 추가 콜백 다음에 실행되며, unmount가 되면 실행되지 않을 수 있다.)
// src/components/
// ...
const { mutate: addTodoMutate } = useAddTodoMutation();
const handleAddTodo = useCallback(() => {
addTodoMutate({
id: 1, todo: 'mutate에서의 추가 콜백'
},
{
onSuccess: (data) => {
alert('Todo added!');
},
});
}, [addTodoMutate]);
return (
// ...
<button onClick={handleAddTodo}>작성 완료</button>
// ...
)
mutation 과 최고의 효율을 자랑하는 invalidateQueries 메소드는 정말 정말 유용하다.
어떨 때 사용할 수 있을까 ? 🤔
바로, 기존 쿼리를 강제로 오래된 데이터로 취급하는 무효화 처리를 하고 싶을 때 쓰면 된다!
<< useQuery 로 return 받은 data 를 그대로 UI에 렌더링 하는 경우의 이야기 입니다. >>
🍃 리액트 쿼리 사용 전
1. todo 를 새로 작성하고 create api 를 요청한다.
2. api 가 성공하고 나면 클라이언트 상태(todo list)에 response(추가한 todo)를 update 해주는 dispatch 같은 액션 함수를 사용해야한다.
🌼 리액트 쿼리 사용 후
1. todo 를 새로 작성하고 create mutation을 실행한다.
2. onSettled 나 onSuccess 콜백 함수에 todo list 쿼리를 invalidateQueries() 작성해주면 알아서 최신 값으로 refetch 된다.
바아로 (1)에 작성했던 코드를 가져와 변형시켜보겠다.
// src/hooks
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
import { queryKeys } from 'src/types/commonType'; // useQuery 실용 편 참조
export default function useAddTodoMutation(): UseMutationResult<TodoType, AxiosError, TodoType> {
const queryClient = useQueryClient();
return useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries(queryKeys.todos); // mutation을 성공하면 todo list를 불러오는 useQuery를 무효화 시킨다.
},
onError: (error) => {
console.error(error);
},
});
}
invalidateQueries
가 실행되어 쿼리가 무효화되면 해당 쿼리는 오래된 것으로 취급 된다.todo list 를 렌더링 하고 있던 컴포넌트는 새로운 todo 가 추가됨과 동시에 바로 해당 키값의 쿼리를 refetch 시키기 때문에
새로고침이나, 클라이언트 상태에 해당 데이터를 업데이트 시켜주지 않아도 된다. (별도로 클라이언트 상태를 건들 작업이 없을 경우에 해당)
refetchActive: false
를 붙여주면 된다.queryClient.invalidateQueries(queryKeys.todos, {
refetchActive: false,
});
queryClient.invalidateQueries 문서
queryClient.invalidateQueries 문서2
💡 mutation 의
optimistic update
는 사용자가 어떠한 액션을 발생시켰을 때 요청이 성공했는지 실패했는지 아직 알 수 없는 상태에서 성공할 것이라고 낙관적으로 가정하고 사용자가 보고 있는 UI 를 먼저 변화시켜주는 것을 말한다. 사용자에게 빠른 경험을 제공할 수 있다는 큰 장점이 있다!
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
import { queryKeys } from 'src/types/commonType'; // useQuery 실용 편 참조
export default function useAddTodoMutation(): UseMutationResult<
TodoType,
AxiosError,
TodoType,
{
previousTodos: TodoType[] | undefined;
}
> {
const queryClient = useQueryClient();
return useMutation(addTodo, {
onMutate: async (newTodo: TodoType) => { // mutate가 호출될 때
// 쿼리를 확실하게 취소하고
await queryClient.cancelQueries(queryKeys.todos);
// 쿼리 상태를 가져온다(이전 값 스냅샷)
const previousTodos = queryClient.getQueryData<TodoType[]>(queryKeys.todos);
if (previousTodos) {
// previousTodos 가 있으면 setQueryData 를 이용하여 즉시 새 데이터로 업데이트 해준다.
queryClient.setQueryData<TodoType[]>(queryKeys.todos, (old) => [
...(old as TodoType[]),
newTodo,
]);
}
return { previousTodos }; // 이전 값을 리턴한다
},
onError: (
err: AxiosError,
variables: TodoType,
context?: { previousTodos: TodoType[] | undefined }
) => {
if (context?.previousTodos) { // error 를 만났을 경우 onMutate에서 반환된 값으로 다시 롤백시켜준다.
queryClient.setQueryData<TodoType[]>(queryKeys.todos, context.previousTodos);
}
},
onSettled: () => { // mutation이 끝나면 (성공유무 상관없이) 쿼리를 무효화 처리하고 새로 가져온다.
queryClient.invalidateQueries(queryKeys.todos);
},
});
}
queryClient.cancelQuerie
: 발신 쿼리를 취소하는 데 사용할 수 있다. (optimistic update를 덮어쓰지 않도록)queryClient.getQueryData
: 기존 쿼리의 상태를 가져오는 동기 함수. 쿼리가 존재하지 않으면 undefined
를 반환.queryClient.setQueryData
: 쿼리의 캐시된 데이터를 즉시 업데이트할 수 있는 동기 함수. 쿼리가 존재하지 않으면 생성된다.optimistic updates 를 이용하면 사용자가 api 요청 성공 유무를 기다리지 않고 바로 UI 단에서 변화를 확인할 수 있어 유용하다.
ex) 페이스북 좋아요를 누른다고 가정할 때 좋아요를 누르거나 취소할 때마다 api 결과 기다려야 한다면 매우 답답할 것이다. 사용자가 좋아요를 마구 눌렀다 취소할 수 도 있는데 이럴 때 특히 유용하다.
모두 리액트 쿼리 쓰세요오오😊
깔끔하게 잘 정리하셨네요 감사합니다