두 메서드의 공통점: fetch 중인 request가 존재할 때에는 값이 true이다.
다만 isLoading
은 isFetching
의 부분집합으로서 그 범위가 더 좁다.
isLoading
이 true인 경우는
1. fetching중이면서
2. 현재 캐싱된 데이터가 없을 때
두 조건을 만족해야 true가 된다.
즉, prefetching으로 데이터를 미리 받아놓은 상황이라면 isLoading
은 false값을 유지한다.
(캐싱데이터는 특별히 설정하지 않는 이상 5분간 유지된다.)
useQuery
react-query는 react의 hooks에 꽤 의존적이다.
currentPage가 바뀔 때마다(정확히는 useQuery의 key가 바뀔때마다) query를 재실행한다.
const { data, isError, error, isLoading, isFetching } = useQuery(
['posts',currentPage],
() => fetchPosts(currentPage),
{
staleTime: 1000,
}
);
prefetching을 통해 다음페이지를 caching 하므로 isLoading
은 cached data가 gc에 의해 사라지기 전까지 계속 false 값을 유지할 수 있다.
Prefetching
const queryClient = useQueryClient();
useEffect(() => {
if (currentPage < maxPostPage) {
const nextPage = currentPage + 1;
queryClient.prefetchQuery(['posts', nextPage], () =>
fetchPosts(nextPage)
);
}
}, [currentPage, queryClient]);
prefetching의 또 다른 사용 예시
export function usePrefetchPost(): void { const queryClient = useQueryClient(); queryClient.prefetchQuery(queryKeys.post, getPosts); }
prefetchQuery
를 커스텀 훅으로 만들어서 해당 컴포넌트에 진입하기 이전에 먼저 필요한 데이터를 받아와 caching 할 수 있다.
즉,getPosts
가 http://localhost:3000/posts에 진입할 때 실행되는 요청이지만, http://localhost:3000에 진입할 때/posts
에 필요한 요청을 사전에(Pre) 진행하게 할 수 있다.
두 메서드는 현재 진행중인 request의 개수를 정수로 나타낸다.
const isFetching = useIsFetching();
const isMutating = useIsMutating();
const display = isFetching || isMutating ? 'inherit' : 'none';
할당한 display
변수의 값을 통해 로딩 스피너를 조정할 수 있다.
<Spinner display={display} />
fallback을 설정하여 초기 data의 상태가 undefined로 인해 error가 발생되는 것'을 방지할 수 있다.
export function useTreatments(): Treatment[] {
// 일일히 isLoading을 해줄 필요 없이 중앙화 할 수 있음.
const fallback = [];
const { data = fallback } = useQuery(queryKeys.something, getData, {
onError: (error) => {
const errorMessage = error instanceof Error
? error.message
: 'error connecting to the server';
toast(errorMessage)
}
});
return data
}
위 fallback에 작성한 onError
는 useTreatments
내에서 작성했기 때문에 해당 훅에만 적용된다.
만약 여러개의 queries가 존재한다면 똑같은 onError
를 작성해야 되는 부분이 많아지기에, 모든 쿼리에 적용할 수 있는 onError
를 기본값으로 정의할 수 있다.
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler
}
}
});
queryClient의 onError를 queryErrorHandler를 기본 값으로 설정했다.
queryErrorHandler는 다음과 같다.
function queryErrorHandler(error: unknown): void {
const id = 'react-query-error';
const title = error instanceof Error
? error.message
: 'error connecting to server';
toast(`${id}: ${title}`);
}
데이터를 변형할 때 사용하는 useMutation
의 defaultOptions도 지정할 수 있다.
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,
}
},
});
useQuery
를 통해 받아오는 data를, 원하는 형식에 맞게 transform 해주는 기능.
useQuery
의 3번째 인자인 options 객체에 입력한다.
React-query의 최적화 (memoization)
Select function은 데이터뿐만 아니라 함수 모두가 변경되었을 경우에만 실행하게 만드는 함수이다.
select function이 변경되지 않는다면useCallback
을 사용하여 재실행하지 않게 만들어 최적화해야 하고, 값이 자주 변경되지 않는 stable function이 적합한다.
export function useStaff(): UseStaff {
const [filter, setFilter] = useState('all');
const selectFn = useCallback(
(unfilteredStaff) => filterByTreatment(unfilteredStaff,filter)
,[filter])
const fallback = [];
const {data: staff = fallback} = useQuery(queryKeys.staff,getStaff,{
select: filter !== 'all' ? selectFn : undefined
})
return { staff, filter, setFilter };
}
filterByTreatment 함수는 다음과 같다.
export function filterByTreatment(staff: Staff[],treatmentName: string): Staff[] {
return staff.filter((person) =>
person.treatmentNames
.map((t) => t.toLowerCase())
.includes(treatmentName.toLowerCase()),
);
}
또한 refetch가 trigger되는 조건들을 off 할 수 있다.
(refetchOnMount,refetchOnWindowFocus,refetchOnReconnect)
export function useTreatments(): Treatment[] {
const fallback = [];
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments,{
staleTime: 600000, // 10 minutes
cacheTime: 3600000, // 1 hours
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
return data
}
자주 변하는 데이터라면 주기적으로 refetching하여 데이터의 변화를 지속적으로 감시할 수 있다.
const { data: appointments = fallback } = useQuery(
[queryKeys.appointments,monthYear.year, monthYear.month],
() => getAppointments(monthYear.year, monthYear.month,
{
refetchInterval: 60000 // every minute;
}
))
쿼리 키와 값을 인수로 받아 존재하는 cached data에 해당 키에 대한 값을 updating 할 수 있게 한다.
setQueryData
와 fetchQuery
의 차이점은, setQueryData
는 동기화이며 이미 동기식으로 데이터를 사용할 수 있다고 가정한다.
데이터를 비동기적으로 가져와야 하는 경우 쿼리 키를 다시 가져오거나 fetchQuery
를 사용하여 비동기로 처리하는 것이 좋다.
function updateUser(newUser: User): void {
queryclient.setQueryData(queryKeys.user, newUser)
}
function clearUser() {
queryClient.setQueryData(queryKeys.user, null);
}
queryFn을 실행하여 data를 받아오거나 ,setQueryData에서 data를 성공적으로 가져왔을 때 후속으로 실행하는 만드는 메서드이다.
만약 로그인 인증 queryFn,setQueryData라면, localStorage에 존재하는 데이터를 load하는 함수를 onSuccess의 콜백으로 입력하여 업데이트 할 수 있다.
const {data: user} = useQuery(queryKeys.user, () => getUser(user),{
onSuccess: (received: User | null) => {
if(!received){
clearStoredUser()
}else{
setStoredUser(received);
}
}
})
초기 데이터를 cache에 추가하고 싶을 때 사용한다.
fallback과 다른점은 fallback은 placeholderData와 같이 실제 캐시에는 추가되지 않지만 initialData는 실제 cache에 추가된다.
const {data: user} = useQuery(queryKeys.user, () => getUser(user),{
initialData:getStoredUser()
})
enabled 프로퍼티에 boolean 값을 통해, 특정 값에 대한 결과를 의존하게 하여 쿼리를 활성화/비활성화 할 수 있게 만든다.
export function useUserAppointments(): Appointment[] {
const {user} = useUser();
const fallback:Appointment[] = [];
const { data: userAppointments = fallback } = useQuery(
"user-appointments",
() => getUserAppointments(user),
{
enabled: !!user
}
)
return userAppointments
}
useQuery와 유사하지만 몇가지 차이점이 존재한다.
Fetcing,Refetching, updateData가 있는 useQuery와는 다르며 기본적으로 재시도가 없다.(useQuery는 기본적으로 3번 재시도한다.)
캐싱할 데이터가 존재하지 않으므로 isLoading과 isFetching이 구분되지 않는다.(정확히는 isLoading이 존재하지 않는다.)
캐싱할 데이터가 존재하지 않기 때문에 쿼리키를 필요로 하지 않는다.
useMutation
은 mutate
함수를 반환하는데 요청을 보내는데에 사용되며
onMutate 콜백도 존재한다. 이것은 optimize query에 사용되며 변이가 실패할 때(http request가 실패할 때) 복원할 수 있도록 이전 상태를 저장하는데 사용할 수 있다.
제네릭에 아래 타입을 순서대로 입력한다.
1. mutate function자체에서 반환되는 데이터타입, 없다면 void처리
2. Error의 타입. customError가 없다면 unknown
3. mutate function에서 사용하는 매개변수 타입
4. context의 타입으로서 optimize rollback을 위해 onMutate에 설정하는 타입
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
}
캐싱된 데이터를 오래된 데이터로 취급변경하고, 재요청을 촉발시킨다.
일반적으로 mutate
를 호출하면 mutation에 작성한 onSuccess
를 통해 관련 커리를 무효화하고 해당 데이터를 재요청하는 방식으로 사용한다.
invalidateQueries는 정확한 keyName이 아니라 prefix를 통해
모든 queries를 무효화 하는데, 정확한 키로 설정하고 싶다면
exact:true
로 설정하면 된다.
export function useReserveAppointment(): UseMutateFunction<void,unknown,Appointment,unknown> {
const { user } = useUser();
const queryClient = useQueryClient();
const {mutate} = useMutation((appointment:Appointment) => setAppointmentUser(appointment,user?.id),{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments])
}
})
return mutate
}
서버로 요청이 전달되는 도중에 취소할 수 있다는 점으로
서버에서 오는 모든 데이터가 캐시의 optimize update를 덮어쓰는 일이
없도록 해야한다.
userQuery키를 가진 useQuery가 AbortController를 signal로 관리하고, AbortController는 쿼리 함수인 getUser에 전달되는 신호를 생성하고, getUser는 해당 신호를 Axios에 전달하여 Axios와 signal을 연결시킨다.
이후 cancelQuery에 AbortController를 관리하는 동일한 키에 실행하는 경우 AbortController에 취소 이벤트를 전달한다.
onMutate 함수는 mutation function(http request)이 실행되기 이전에 작동하며 mutation function이 받는 똑같은 변수를 전달받는다.
동일한 데이터를 사용하는 컴포넌트가 다수 존재하며, 서버에서 업데이트가 오래 걸릴 경우 아주 강력한 도구가되며, 사용자 측에서는 훨씬 반응성이 좋게 느껴지게 할 수 있다.
onMutate 함수의 return 값은 mutation function이 실패할 시 onError및 onSettled 함수에 return값이 전달되며 Optimistic Update를 롤백하는 데 유용하다.
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는 onError에 전달된 컨텍스트를 반환한다.
onMutate: async(newData: User | null) => {
// user를 대상으로 발신하는 쿼리를 모두 취소하게 하여
// 이전 서버 데이터가 optimize update를 덮어쓰지 않게 abortController를 실행하게 한다.
queryClient.cancelQueries(queryKeys.user)
// 이전 user value의 snapshot
const prevUserData:User = queryClient.getQueryData(queryKeys.user);
// 새로운 user 값으로 캐시를 optimistically update하고,
updateUser(newData)
// snapshot value가 있는 컨텍스트 객체 반환
return {prevUserData}
},
onError:(error,newData,context) => {
// 에러가 발생한다면 캐시를 저장된 값으로 롤백한다.
if(context.prevUserData){
updateUser(context.prevUserData);
toast({
title: 'Update failed; restoring previous data',
status: 'warning'
})
}
},
onSuccess: (userData: User | null) => {
if(user){
toast({
title: "user updated!",
status: 'success'
})
}
},
// onSettled: mutate의 성공 여부와 관계 없이 onSettled에 작성된 callback이 실행된다.
onSettled: () => {
// user 데이터를 무효화하고 서버에서 최신 데이터를 받아 와 보여줄 수 있게 작성한다.
queryClient.invalidateQueries(queryKeys.user);
}
}
)
return patchUser;
}