[React Query] Section4 - React Query in Larger App: Setup, Centralization, Custom Hooks

이해용·2022년 8월 28일
1
post-thumbnail

※ 해당 섹션은 3버전 react query로 4버전과 다른 부분이 있을 수 있습니다.

Custom Hooks

  • in larger apps, make custom hook for each type of data
    • can access from multiple components (다수의 컴포넌트에서 데이터를 액세스 해야하는 경우 useQuery 호출을 재작성 할 필요가 없습니다.)
    • no risk of mixing up keys (다수의 useQuery 호출을 사용했다면 사용 중인 키의 종류가 헷갈리 수 있습니다. 따라서 커스텀 훅을 사용해 매번 호출한다면 키를 헷갈릴 위험이 없습니다.)
    • query function encapsulated in custom hook (또한, 사용하길 원하는 쿼리함수를 혼동하는 위험도 없습니다. 커스텀 훅에 넣어주면 다수의 컴포넌트에 굳이 불러올 필요가 없습니다.)
    • abstracts implementation from display layer (또한, 일반적으로 디스플레이 레이어에서 데이터를 어떻게 가져오는가에 대한 구현을 추상화합니다.
      • update hook if you change implementation (즉 구현을 변경하기로 결정했다면)
      • no need to update components (컴포넌트는 업데이트할 필요 없이 훅을 업데이트하기만 하면 됩니다.)
//useTreatment.ts

import { useQuery } from '@tanstack/react-query';

import type { Treatment } from '../../../../../shared/types';
import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
import { useCustomToast } from '../../app/hooks/useCustomToast';

async function getTreatments(): Promise<Treatment[]> {
  const { data } = await axiosInstance.get('/treatments');
  return data;
}

export function useTreatments(): Treatment[] {
  const { data } = useQuery([queryKeys.treatments], getTreatments);
  return data;
}

결과

map error

fallback data

기존의 작업 방식에서는 isLoading, isError를 각각의 컴포넌트에서 진행했으나 custom hooks을 사용할 때는 중앙에서 처리할 수 있도록 설정해줍니다. 대신 다른 방법으로 처리를 합니다.

export function useTreatments(): Treatment[] {
  const fallback = []; // fallback이 빈 배열이라고 설정. 서버에서 treatments 데이터를 받지 않고 캐시가 비어있는 경우 아무 것도 표시하지 않도록 해줍니다.
  const { data = fallback } = useQuery([queryKeys.treatments], getTreatments);
  return data;
}

fallback 결과

useIsFetching

  • In smaller apps
    • used isFetching from useQuery return object (useQuery 리턴 객체에서 isFetching 을 사용했습니다. useQuery 리턴 객체에서 isFetching의 구조를 분해했습니다.)
    • Reminder: isLoading is isFetching plus no cached data (isLoading은 isFetching의 캐시된 데이터가 없는 것과 같습니다. isFetching은 큰 항목, isLoading은 작은 항목으로 가져오기를 하면서 해당 쿼리에 대한 캐시된 데이터가 없는 경우입니다.)

isLoading VS isFetching

  • In a larger app

    • Loading spinner whenever any query isFetching (어떠한 쿼리가 데이터를 가져오는 중일 때 로딩 스피너를 표시하면 좋습니다. 앱 컴포넌트의 일부로서 중앙화된 로딩 스피너를 확보하도록 합니다. 쿼리가 가져오기 중인 경우 켜도록하고 가져오는 중인 쿼리가 없는 경우 끄도록 합니다.)
    • useIsFetching tells us this (useIsFetching은 현재 가져오기 중인 쿼리가 있는지를 우리에게 알려주는 훅입니다.)
  • No need for isFetching on every custom hook / useQuery call (즉 각각의 커스텀 훅에 대해 isFetching을 사용할 필요가 없습니다. 대신 useIsFetching 훅을 로딩 컴포넌트에 사용할 수 있고 useIsFetching의 값은 스피너의 표시 여부를 우리에게 알려줄 것입니다.)

// app.tsx

...
export function App(): ReactElement {
  return (
    <ChakraProvider theme={theme}>
      <QueryClientProvider client={queryClient}>
        <Navbar />
        <Loading /> // Loading 중일 때는 Loading한다는 표시를 나타낸다.
        <Routes />
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </ChakraProvider>
  );
}

// Loading.tsx

import { Spinner, Text } from '@chakra-ui/react';
import { useIsFetching } from '@tanstack/react-query';
import { ReactElement } from 'react';

export function Loading(): ReactElement {
  const isFetching = useIsFetching(); 

  const display = isFetching ? 'inherit' : 'none';

  return (
    <Spinner
      thickness="4px"
      speed="0.65s"
      emptyColor="olive.200"
      color="olive.800"
      role="status"
      position="fixed"
      zIndex="9999"
      top="50%"
      left="50%"
      transform="translate(-50%, -50%)"
      display={display}
    >
      <Text display="none">Loading...</Text>
    </Spinner>
  );
}
  • useIsFetching은 현재 가져오기 상태인 쿼리 호출의 수를 나타내는 정수값을 반환합니다. isFetching이 0보다 크다면 가져오기 상태인 호출이 존재하며 참으로 평가될 것입니다.
  • 이 경우 display는 inherit으로 설정되어 로딩 스피너를 표시하게 됩니다.
  • 현재 가져오는 항목이 없다면 isFetching은 거짓으로 평가되는데 0이 거짓이기 때문입니다.

Passing errors to toasts(useQuery에 대한 onError 핸들러)

  • Pass useQuery errors to Chakra UI “toast”
    • First for single call, then centralized
  • onError callback to useQuery
    • Instead of detructuring isError, error from useQuery return
    • runs if query function throws an error
    • error parameter to callback
  • Will use toasts
    • Chakra UI comes with a handy useToast hook
// useTreatments.ts

...

export function useTreatments(): Treatment[] {
  const toast = useCustomToast();

  const fallback = [];
  const { data = fallback } = useQuery([queryKeys.treatments], getTreatments, {
    onError: (error) => {
      const title =
        error instanceof Error
          ? error.message
          : 'error connecting to the server';
      toast({ title, status: 'error' });
    },
  });
  return data;
}
  • onError: (error: TError) => void
    • Optional
    • This function will fire if the query encounters an error and will be passed the error. (이 함수는 쿼리에 오류가 발생하고 오류가 전달되면 실행됩니다.)

network error

QueryClient default onError option(쿼리 클라이언트에 대한 onError 기본 값)

  • No useError analogy for useIsFetching (useError 훅이 없는 이유: 정수 이상의 값이 반환되야 하니까 사용자에게 오류를 표시하려면 각 오류에 대한 문자열이 필요한데)
    • need more than interger; unclear how to implement (각기 다른 문자열을 가진 오류가 시시각각 팝업하도록 구현하긴 쉽지 않을 것입니다.)
  • Instead, set default onError handler for QueryClient (QueryClient를 위해 onError 핸들러 기본 값을 만듭니다.)
    • defaults for QueryClient (일반적으로 QueryClient는 쿼리나 변이(Mutation)에 대해 기본 값을 가질 수 있습니다.)
{
	queries: { useQuery options },
	mutations: { useMutation options }
}
// queryClient.ts

...

function queryErrorHandler(error: unknown): void {
  const id = 'react-query-error';
  const title =
    error instanceof Error ? error.message : 'error connecting to server';

  toast.closeAll();
  toast({ id, title, status: 'error', variant: 'subtle', isClosable: true });
}

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: queryErrorHandler,
    },
  },
});

// useTreatments.ts

export function useTreatments(): Treatment[] {
  const fallback = [];
  const { data = fallback } = useQuery([queryKeys.treatments], getTreatments);
  return data;
}

Alternative to onError: Error Boundary

Custom hook for staff

// useStaff.ts

import { useQuery } from '@tanstack/react-query';
import { Dispatch, SetStateAction, useState } from 'react';

import type { Staff } from '../../../../../shared/types';
import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
import { filterByTreatment } from '../utils';

async function getStaff(): Promise<Staff[]> {
  const { data } = await axiosInstance.get('/staff');
  return data;
}

interface UseStaff {
  staff: Staff[];
  filter: string;
  setFilter: Dispatch<SetStateAction<string>>;
}

export function useStaff(): UseStaff {
  const [filter, setFilter] = useState('all');

  const fallback = []; // staff => fallback으로 이름 변경
  const { data: staff = fallback } = useQuery([queryKeys.staff], getStaff); // 구조 분해 프로퍼티의 이름을 data에서 staff 로 바꾼다.
	// 쿼리 키는 queryKeys 상수의 staff 프로퍼티이며 쿼리 함수는 getStaff 입니다.
  return { staff, filter, setFilter }; // 반환 객체에 staff를 반환할 수 있도록 만든다.
}

결과

staff 결과

Section Summary

  • Create file for QueryClient
    • keep code separate (코드를 분리하기 위해 QueryClient 파일을 만들었습니다. onError 콜백을 생성하고 더 많은 옵션 기본값을 추가할 예정이기 때문에 앱 컴포넌트가 아닌 분리된 파일에 가지고 있으면 좋습니다.)
  • Custom hook keeps code modular (데이터가 하나 이상의 컴포넌트에 사용될 때 코드를 모듈식으로 관리할 수 있습니다.
  • Centralize loading component (loading component 집중화. useIsFetching 훅이 불리언을 반환해서 쿼리를 가져오고 있는지 알려줬습니다.)
    • useIsFetching (이 값을 이용해 로딩스피너의 디스플레이 속성을 켰다 껐다 할 수 있습니다.)
  • Centralize error handling (에러 핸들링을 집중화 하고 onError 콜백을 이용해 toast를 생성했습니다.)
    • default onError callback (useQuery 호출에서 분해하는 isError와 error에 의존하지 않아도 됩니다. useTreatment 커스텀 훅 내 useQuery에 onError에 대한 콜백 옵션을 만든 다음 집중화 시키고 QueryClient 옵션 기본 값으로 만들었습니다. 그러면 useQuery를 호출 시 다른 옵션으로 덮어쓰지 않는 이상 모든 useQuery가 해당 OnError 콜백을 사용합니다.)

reference
https://www.udemy.com/course/learn-react-query
https://tanstack.com/query/v4/docs/devtools?from=reactQueryV3&original=https://react-query-v3.tanstack.com/devtools
https://create-react-app.dev/docs/adding-custom-environment-variables
https://chakra-ui.com/docs/components/toast
https://tanstack.com/query/v4/docs/reference/useQuery?from=reactQueryV3&original=https://react-query-v3.tanstack.com/reference/useQuery
https://reactjs.org/docs/error-boundaries.html

profile
프론트엔드 개발자입니다.

0개의 댓글