※ 본 강의에서의 react query는 3버전으로 4버전과는 버전 차이가 있어 코드가 다를 수 있습니다.
onError
callback in mutations
property of query client defaultOptions
useIsMutating
is analogous to useIsFetching
Loading
component to show on isMutating
// queryClient.ts
...
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
staleTime: 600000, // 10 minutes
cacheTime: 900000, // 15 minutes,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
mutations: {
onError: queryErrorHandler,
},
},
});
// Loading.tsx
...
export function Loading(): ReactElement {
const isFetching = useIsFetching();
const isMutating = useIsMutating(); // 현재 해결되지 않은 변이 함수의 개수를 정수로 볼 수 있습니다.
const display = isFetching || isMutating ? 'inherit' : 'none';
...
useQuery
isLoading
vs isFetching
(캐시 데이터가 없으므로 isLoading과 isFetching이 구분되지 않습니다. isLoading은 데이터가 없을 때 이루어지는 페칭이기 때문입니다. useMutation에는 캐시 데이터 개념이 없으므로 isLoading 개념이 없습니다. isFetching만 있습니다.)mutate
function which actually runs mutation (useMutation은 반환 객체에서 mutate 함수를 반환합니다. 그리고 이것이 변이를 실행하는 데 사용됩니다.onMutate
callback (useful for optimistic queries!) (변이가 실패 할 때 복원할 수 있도록 이전 상태를 저장하는데 사용할 것입니다.)// useReserveAppointment.ts
...
async function setAppointmentUser(
appointment: Appointment,
userId: number | undefined,
): Promise<void> {
if (!userId) return;
const patchOp = appointment.userId ? 'replace' : 'add';
const patchData = [{ op: patchOp, path: '/userId', value: userId }];
await axiosInstance.patch(`/appointment/${appointment.id}`, {
data: patchData,
});
}
type AppointmentMutationFunction = (appointment: Appointment) => void;
export function useReserveAppointment(): AppointmentMutationFunction {
const { user } = useUser();
const toast = useCustomToast();
const { mutate } = useMutation((appointment) =>
setAppointmentUser(appointment, user?.id),
);
return mutate; // TypeScript Error
}
appointment와 userId 모두 필요합니다. 서버의 데이터베이스를 업데이트하려면 어떤 사용자가 어떤 예약을 했는지 알아야합니다.
Type for returning mutate
function from custom hook
useMutateFunction<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>
Data Type returned by mutation function
Example: void
(변이 함수 자체에서 반환된 데이터 유형입니다. 이 경우 변이 함수는 데이터를 반환하지 않습니다. 따라서 void로 설정합니다.)
Error type thrown by mutation function
Example: Error
(변이 함수에서 발생할 것으로 예상되는 오류(Error) 유형입니다.
mutate
function variables type
Example: Appointment
(mutate 함수가 예상하는 변수 유형입니다.)
Context type set in onMutate
function for optimistic update rollback
Example: Appointment
(낙관적 업데이트 롤백을 위해 onMutate에 설정하는 유형입니다.)
// useReserveAppointment.ts
export function useReserveAppointment(): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> {
const { user } = useUser();
const toast = useCustomToast();
const { mutate } = useMutation((appointment: Appointment) =>
setAppointmentUser(appointment, user?.id),
);
return mutate;
}
invalidateQueries
effects:mutate—onSuccess—>invalidateQueries—>re-fetch
// useReserveAppointment.ts
export function useReserveAppointment(): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> {
const { user } = useUser();
const toast = useCustomToast();
const queryClient = useQueryClient();
const { mutate } = useMutation(
(appointment: Appointment) => setAppointmentUser(appointment, user?.id),
{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]);
toast({
title: 'You have reserved the appointment!',
status: 'success',
});
},
},
);
return mutate;
}
9월 5일 예약을 해도 흰색으로 예약은 되었으나 Your Appointments에 자동으로 업데이트 되지 않고 새로고침을 해야만 예약 내역이 업데이트 됩니다.
invalidateQueries
take a query key prefix (invalidateQueries는 정확한 키가 아닌 접두사를 사용합니다.){ exact: true }
option (정확한 키로 설정하고 싶다면 exact: true로 설정하면 됩니다.)removeQueries
) (다른 queryClient 메서드도 removeQueries와 같은 쿼리 키 접두사를 사용합니다.)For user appointments
[ queryKeys.appointments, querykeys.user, user?.id ]
For appointments
[ queryKeys.appointments, queryMonthYear.year, queryMonthYear.month ]
Pass [ queryKeys.appointments ]
as prefix to invalidateQueries
For more complicated apps, use functions to create query keys to ensure consistency
reference
// useUserAppointments.ts
...
export function useUserAppointments(): Appointment[] {
const { user } = useUser();
const fallback: Appointment[] = [];
const { data: userAppointments = fallback } = useQuery(
[queryKeys.appointments, queryKeys.user, user?.id], // 'user-appointments' 에서 Query Key Prefix로 변경
() => getUserAppointments(user),
{ enabled: !!user },
);
return userAppointments;
}
// useUser.ts
...
function clearUser() {
queryclient.setQueryData(queryKeys.user, null);
queryclient.removeQueries([queryKeys.appointments, queryKeys.user]); // 'user-appointments' 에서 Query Key Prefix로 변경
}
...
useUser
// 나의 코드
...
export function useCancelAppointment(): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> {
const toast = useCustomToast();
const queryClient = useQueryClient();
const { mutate } = useMutation(
(appointment: Appointment) => { // 에러 발생
removeAppointmentUser(appointment);
},
{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]);
toast({
title: 'You have canceled the appointment!',
status: 'success',
});
},
},
);
return mutate;
}
// 실제 코드
export function useCancelAppointment(): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> {
const toast = useCustomToast();
const queryClient = useQueryClient();
const { mutate } = useMutation(removeAppointmentUser, {
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]); // 모든 쿼리 무효화
toast({
title: 'You have canceled the appointment!',
status: 'warning',
});
},
},
);
return mutate;
}
appointment만 가져와서 사용하기 때문에 불필요한 익명함수를 removeAppointmentUser만으로 사용 가능합니다.
변이함수 ⇒ onSueccess
New custom hook usePatchUser
(서버에서 사용자를 업데이트하는 데 사용할 메서드입니다.)
Use the handy updateUser
function from useUser
localStorage
reference
// usePatchUser.ts
...
export function usePatchUser(): UseMutateFunction<
User,
unknown,
User,
unknown
> {
const { user, updateUser } = useUser();
const toast = useCustomToast();
const { mutate: patchUser } = useMutation(
(newUserData: User) => patchUserOnServer(newUserData, user),
{
onSuccess: (userData: User | null) => {
if (user) {
updateUser(userData);
toast({
title: 'User updated!',
status: 'success',
});
}
},
},
);
return patchUser;
}
useMutation
han as onMutate
callback
onError
for rollback (콘텍스트 값을 반환하고 onError 핸들러가 이 콘텍스트 값을 인수로 받습니다. 에러가 생기면 onError 핸들러가 호출되고 onError 핸들러가 콘텍스트 값을 받아서 캐시 값을 이전으로 복원할 수 있게 되죠)onMutate
function can also cancel refetches-in-progress (캐시를 업데이트할 데이터를 포함하는 특정 쿼리에서 onMutate 함수는 진행 중인 모든 리페치(Refetch)를 취소합니다.)
reference: https://tanstack.com/query/v4/docs/guides/optimistic-updates
User trigger update with mutate
⇒ - Send update to server
onMutate
Cancel queries in progress
Update query cache
Save previous cache value
⇒(success?) —yes—> invalidate query
⇒(success?) —no—> onError
uses context to roll back cache
Query functions haven’t been cancelable by React Query
In order to cancel from React Query, query function must:
cancel
property that cancels queryreference: https://tanstack.com/query/v4/docs/guides/query-cancellation
signal
to axios via argument to query function (axios에 중단한다는 신호를 전달해야합니다. 이 중단한다는 신호를 쿼리 함수에 인수로 전달됩니다.)// useUser.ts
...
async function getUser(
user: User | null,
signal: AbortSignal, // signal에 AbortSignal 타입 지정
): Promise<User | null> {
if (!user) return null;
const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get(
`/user/${user.id}`,
{
headers: getJWTHeader(user),
signal, // signal을 axios 인스턴스의 한 구성으로 전달할 수 있습니다.
},
);
return data.user;
}
...
export function useUser(): UseUser {
const queryclient = useQueryClient();
const { data: user } = useQuery(
queryKeys.user,
({ signal }) => getUser(user, signal),
{
initialData: getStoredUser,
onSuccess: (received: User | null) => {
if (!received) {
clearStoredUser();
} else {
setStoredUser(received);
}
},
},
);
...
getUser에 axios 호출을 전달 받기 위해 useQuery가 쿼리 함수에 전달하는 인수로부터 구조분해를 진행하여 이루어집니다.
즉, useQuery는 인수들의 객체를 전달하며 이 신호의 구조를 분해한 뒤 getUser에 두번 째 인수로 전달하는 것입니다.
signal
useQuery(queryKeys.user) → AbortController—signal—> getUser—signal—>axios
queryClient.cancelQuery(queryKeys.user)—cancel—>AbortController
사용자 쿼리 키를 지닌 useQuery가 AbortController를 관리합니다. 이 컨트롤러는 쿼리 함수인 getUser에 전달되는 신호를 생성하고 getUser는 해당 신호를 Axios에 전달합니다. 따라서 이제 Axios는 해당 신호에 연결된 상태입니다. 취소 이벤트에 대하여 해당 신호를 수신하는 것이죠.
// usePatchUser.ts
...
export function usePatchUser(): UseMutateFunction<
User,
unknown,
User,
unknown
> {
const { user, updateUser } = useUser();
const toast = useCustomToast();
const queryClient = useQueryClient();
const { mutate: patchUser } = useMutation(
(newUserData: User) => patchUserOnServer(newUserData, user),
{
// onMutate returns context that is passed to onError
onMutate: async (newData: User | null) => {
// cancel any outgoing queries for user data, so old server data
// doesn't overwrite our optimistic update
queryClient.cancelQueries(queryKeys.user);
// snapshot of previous user value
const previousUserData: User = queryClient.getQueryData(queryKeys.user);
// optimistically update the cache with new user value
updateUser(newData);
// return context object with snapshotted value
return { previousUserData };
},
onError: (error, newData, context) => {
// roll back cache to saved value
if (context.previousUserData) {
updateUser(context.previousUserData);
toast({
title: 'Update failed; restoring previous values',
status: 'warning',
});
}
},
onSuccess: (userData: User | null) => {
if (user) {
updateUser(userData);
toast({
title: 'User updated!',
status: 'success',
});
}
},
onSettled: () => {
// invalidate user query to make sure we're in sync with server data
queryClient.invalidateQueries(queryKeys.user);
},
},
);
return patchUser;
}
setQueryData
쿼리 클라이언트인 setQueryData를 사용하여 변이 함수로부터 반환된 데이터로 캐시를 업데이트할 수 있습니다.)reference
https://www.udemy.com/course/learn-react-query
https://tanstack.com/query/v4/docs/reference/useIsMutating
https://tanstack.com/query/v4/docs/reference/useMutation
https://tanstack.com/query/v4/docs/guides/mutations
https://tanstack.com/query/v4/docs/guides/query-keys
https://tanstack.com/query/v4/docs/guides/query-invalidation#query-matching-with-invalidatequeries
https://tanstack.com/query/v4/docs/guides/updates-from-mutation-responses
https://tanstack.com/query/v4/docs/guides/optimistic-updates
https://tanstack.com/query/v4/docs/guides/query-cancellation
https://axios-http.com/docs/cancellation
https://developer.mozilla.org/ko/docs/Web/API/AbortController
https://developer.mozilla.org/en-US/docs/Web/API/AbortController