[react-query & typescript] custom hook으로 빼고 싶은데 타입을 어떡하지?

movie·2022년 7월 13일
16

useQuery, useMutation 등 API 로직을 컴포넌트에서 분리해서, custom hook으로 빼고 싶은데 타입을 어떡하지?..
너무 너무 복잡하다 🤣

🔥 왜 custom hook으로 뺄까요?

  • query를 재사용할 때 default options를 설정할 수 있다.
  • 컴포넌트에서 복잡한 API 로직(데이터 정제와 같은)과 query key를 관리하지 않아도 된다.
  • 코드 참고

1️⃣ useQuery

// 우리 서비스 코드 
// usePost

import { useQuery, QueryKey, UseQueryOptions } from 'react-query';
import axios, { AxiosError, AxiosResponse } from 'axios';
import QUERY_KEYS from '@/constants/queries';

// 특정 id의 post를 가져오는 query
const usePost = ({
  storeCode,
  options,
}: {
  storeCode: QueryKey; // query key에 넣어줄 배열값
  options?: UseQueryOptions<AxiosResponse<Post>, AxiosError, Post, QueryKey[]>; // useQuery의 options
}) =>
  useQuery([QUERY_KEYS.POST, storeCode], ({ queryKey: [_, id] }) => axios.get(`/posts/${id}`), {
    select: data => data.data,
    ...options,
  });

export default usePost;

여기서 막힌점

storeCode와 options의 타입을 알아야한다.

  1. storeCode : QueryKey 타입
type QueryKey = string | readonly unknown[]
  1. options

UseQueryOptions 라는 타입이 지원된다.

export interface UseQueryOptions
  <TQueryFnData = unknown, 
   TError = unknown, 
   TData = TQueryFnData, 
   TQueryKey extends QueryKey = QueryKey> extends UseBaseQueryOptions ...
{
}

대충 이렇게 제네릭이 이루어져 있다.

useQueryOptions extends UseBaseQueryOptions extends QueryObserverOptions .. 이므로 따라 따라 가보면

오 드디어 옵션들이 보인다..!

query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
onError?: (err: TError) => void;
onSuccess?: (data: TData) => void;
// 뇌피셜

TQueryFnData : query function이 뱉는 data니깐 AxiosResponse
TError : onError의 매개변수로 들어가는 값의 타입이므로 AxiosError
TData : useQuery가 뿜는 data니깐 TQueryFnData와 같거나 정제된 데이터 
TQueryKey : 지원되는 QueryKey타입을 쓰면 되겠다. 어차피 default type이 QueryKey인듯

2️⃣ useMutation

// 우리 서비스 코드
// useCreatePost

import { useContext } from 'react';
import { useQueryClient, useMutation, UseMutationOptions } from 'react-query';
import axios, { AxiosError, AxiosResponse } from 'axios';
import SnackbarContext from '@/context/Snackbar';
import QUERY_KEYS from '@/constants/queries';

const useCreatePost = (
  options?: UseMutationOptions<AxiosResponse<string, string>, AxiosError, Pick<Post, 'title' | 'content'>>,
) => {
  const queryClient = useQueryClient();
  const { showSnackbar } = useContext(SnackbarContext);

  return useMutation(
    ({ title, content }: { title: string; content: string }): Promise<AxiosResponse<string, string>> =>
      axios.post('/posts', {
        title,
        content,
      }),
    {
      ...options,
      onSuccess: (data, variables, context) => {
        queryClient.resetQueries(QUERY_KEYS.POSTS);
        showSnackbar('글 작성에 성공하였습니다.');

        if (options && options.onSuccess) {
          options.onSuccess(data, variables, context);
        }
      },
    },
  );
};

export default useCreatePost;

이번에도 options의 타입을 알아야 한다.

useMutation의 options를 위해 UseMutationOptions 타입이 지원된다.
이번에는 깃허브를 참고해서 타입을 추론해봤다.

export interface UseMutationOptions<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
> {
  mutationKey?: string | unknown[]
  onMutate?: (variables: TVariables) => Promise<TContext> | Promise<undefined> | TContext | undefined
  onSuccess?: (
    data: TData, // 1️⃣
    variables: TVariables,
    context: TContext | undefined
  ) => Promise<void> | void
  onError?: (
    error: TError, // 2️⃣
    variables: TVariables,
    context: TContext | undefined
  ) => Promise<void> | void
  onSettled?: (
    data: TData | undefined,
    error: TError | null,
    variables: TVariables,
    context: TContext | undefined
  ) => Promise<void> | void
  retry?: RetryValue<TError>
  retryDelay?: RetryDelayValue<TError>
  useErrorBoundary?: boolean
}

3️⃣

// 뇌피셜
TData : 1️⃣ useMutation이 뱉는 data, AxiosResponse아니면 정제된 데이터일 것이다. 
TError : 2️⃣ AxiosError 
TVariables : 3️⃣ 잘 모르겠어서 출력해봤다.. request body로 보내준 데이터가 출력이 되었다. 그래서 그 데이터의 타입을 입력해주었다. 
TContext : onMutate가 리턴하는 객체를 context 파라미터로 참조가 가능하다고 하다. 쓰이지 않아서 unknown으로 냅뒀다. 

3️⃣ AxiosResponse도 generic이던데..

//AxiosResponse<T, D>

export interface AxiosResponse<T = any, D = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}

export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method | string;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: AxiosRequestHeaders;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: D;
  ..

이것도 잘 가늠이 안돼서 출력해보았다.!..!

아하!

도대체 타입추론하는 쉬운 방법은 뭘까? 하하



참고

profile
영화보관소는 영화관 😎

1개의 댓글

comment-user-thumbnail
2022년 10월 4일

좋은 글 감사합니다!

매번 커스텀 훅으로 useQuery를 만들 때 마다, 작성해주신 타입을 지정해서 사용하면 코드가 좀 길어지고 번거로워서

interface UseQueryOptionsType<T>
  extends UseQueryOptions<AxiosResponse<T>, AxiosError, T, QueryKey[]> {}

와 같은 형태로 두고, 사용하면 더 편하더라구요!

답글 달기