Skip to content

useGetProducts & useGetRecommendsProducts 훅 #61

@hwisaac

Description

@hwisaac

설명

useInfiniteQuery 를 이용해 페이지네이션된 데이터를 가져옵니다.

const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useGetProducts(accessToken, options?)
const {
  data,
  isLoading,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useGetRecommendsProducts(accessToken, options?)

사용예시

// home 에 적용됐습니다. 

import useGetRecommendProducts from '@/libs/hooks/useGetRecommendsProducts';

const Main = () => {
  const [products, setProducts] = useState([]); 
  const {
    data,
    isLoading: fetchingRecommends,
    fetchNextPage,
    hasNextPage,
  } = useGetRecommendProducts(accessToken, {
    onSuccess: (data) => {
      let result = [];
      for (let page of data.pages) {
        result = [...result, ...page.content];
      }
      setProducts(result);
    },
  });

return ( 
  {products?.map((product) => (
    <LoanProduct key={product.id} product={product} />
  ))}
)

정의

두 훅이 같은 방식으로 정의됐습니다.

import { useInfiniteQuery } from '@tanstack/react-query';
import React from 'react';
import { ax } from '../axiosClient';

const useGetRecommendProducts = (accessToken, options?) => {
  return useInfiniteQuery(
    ['products'],
    ({ pageParam = 1 }) => ax.getRecommendsProducts(accessToken, pageParam),
    Object.assign(
      {
        getNextPageParam: (lastPage, allPages) => {
          if (lastPage.pageNumber >= lastPage.totalPages) return;
          return lastPage.pageNumber + 1;
        },
      },
      options
    )
  );
};

export default useGetRecommendProducts;

useInfiniteQuery

  • 이미 가지고 있는 데이터 세트에다가 추가적으로 데이터를 로드 해야될 경우 사용한다. (ex: 무한스크롤 or 페이지네이션 )
  • useInfiniteQuery 를 사용할 때 알아야 하는 사실들
    1. data 는 현재 infinite query data를 포함하는 객체이다.
    2. data.pages 는 패치받아온 페이지들로 이뤄진 배열이다.
    3. data.pageParams 는 페이지들을 패치할때 사용한 page params 로 이뤄진 배열이다.
    4. fetchNextPagefetchPreviousPage 눈 둘다 'now available'인 함수들
    5. getNextPageParamgetPreviousParam 은 만약 추가로 더 가져올 데이터가 있는지 판단하는 옵션들이다. 이 정보는 쿼리 함수에 추가 파라미터로서 제공된다. (쿼리함수는 fetchNextPage와 fetchPreviousPage 호출로 덮어쓸 수도 있다.)
    6. hasNextPage/hasPreviousPage 는 'now available'인지 나타내는 boolean 이다.(getNextPageParam/getPreviousPageParamundefined를 리턴하지 않을 경우 true가 된다는 말)
    7. isFetchingNextPageisFetchingPreviousPage 는 백그라운드의 refresh state 인지 state를 추가로 load할 수 있는지를 나타내는 boolean이다

예시

// cursor 인덱스로 한번에 페이지를 세개씩 가져온다고 할 때

fetch('/api/projects?cursor=0')
 // { data: [...], nextcursor: 3}
 fetch('/api/projects?cursor=3')
 // { data: [...], nextcursor: 6}
 fetch('/api/projects?cursor=6')
 // { data: [...], nextcursor: 9}
 fetch('/api/projects?cursor=9')
 // { data: [...] }
  • 이러한 정보로 Load More 버튼 같은 걸 만들 수 있다.

주의!

getNextPageParam에서 리턴한 pageParam 데이터를 덮어쓰기를 원하는게 아니라면, fetchNextPage를 콜백 인자로 넣어서 호출하지 말자!

예를들어, <button onClick={fetchNextPage} /> 이런식으로 쓰지 말자.

import { useInfiniteQuery } from 'react-query'
 
 function Projects() {
   const fetchProjects = ({ pageParam = 0 }) =>
     fetch('/api/projects?cursor=' + pageParam)
 
   const {
     data,
     error,
     fetchNextPage,
     hasNextPage,
     isFetching,
     isFetchingNextPage,
     status,
   } = useInfiniteQuery('projects', fetchProjects, {
     getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
   })
 
   return status === 'loading' ? (
     <p>Loading...</p>
   ) : status === 'error' ? (
     <p>Error: {error.message}</p>
   ) : (
     <>
       {data.pages.map((group, i) => (
         <React.Fragment key={i}>
           {group.projects.map(project => (
             <p key={project.id}>{project.name}</p>
           ))}
         </React.Fragment>
       ))}
       <div>
         <button
           onClick={() => fetchNextPage()}
           disabled={!hasNextPage || isFetchingNextPage}
         >
           {isFetchingNextPage
             ? 'Loading more...'
             : hasNextPage
             ? 'Load More'
             : 'Nothing more to load'}
         </button>
       </div>
       <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
     </>
   )
 }

사용예시2

import {  useSearchParams } from "react-router-dom";
import { useInfiniteQuery } from "react-query";

// fetcher 함수
function searchData(
  category: string,
  keyword: string,
  page: number
) {
  const json = await fetch(
    `${BASE_PATH}/search/${category}?api_key=${API_KEY}&query=${keyword}&page=${page}`
  ).then((response) => response.json());
  
  return json;
}


export default function Search() {
// 검색어를 제출하면 Search 가 랜더링 되는데, 검색어 키워드를 가져온다.
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("keyword");

// 랜더링 될 영화들이 담길 state
const [movies, setMovies] = useState();

// useInfiniteQuery 적용
const {
    data: movieInfiniteData,
    fetchNextPage: movieFetchNextPage,
    hasNextPage: movieHasNextPage,
    isFetchingNextPage: movieIsFetchingNextPage,
  } = useInfiniteQuery(
    ["searchMovie", keyword],
    ({ pageParam = 1 }) => searchData("movie", keyword, pageParam),
    {
      getNextPageParam: (lastPage, pages) => {
        const { page, total_pages } = lastPage;
        return page < total_pages ? page + 1 : undefined;
      },

      onSuccess: (movieInfiniteData) => {
        let result = [];
        for (let page of movieInfiniteData.pages) {
          result = [...result, ...page.results];
        }
        setMovies(result);
      },
    }
  );

return (
<Wrapper>
  <Title>Movie SEARCH: {keyword}</Title>

  {movies && <SearchedResults results={movies} />}

  <MoreButton
    onClick={() => movieFetchNextPage()}
    disable={!movieHasNextPage || movieIsFetchingNextPage}>
    {movieIsFetchingNextPage
      ? "추가 패칭중"
      : movieHasNextPage
      ? "영화 더보기"
      : "남은 영화가 없습니다."}
  </MoreButton>
</Wrapper>
);
}

useInfiniteQuery Hook

const {
   fetchNextPage,
   fetchPreviousPage,
   hasNextPage,
   hasPreviousPage,
   isFetchingNextPage,
   isFetchingPreviousPage,
   ...result
 } = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
   ...options,
   getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
   getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
 })

옵션

  • useInfiniteQuery 의 옵션들은 useQuery 훅이랑 같은데, 아래 내용이 추가된다.
옵션명 타입 default 설명
queryFn (context: QueryFunctionContext) => Promise defaultQueryFn - defaultQueryFn이 없는 경우에 필수
- 데이터 요청할때 사용하는 쿼리 함수
QueryFunctionContextqueryKeypageParam:unknown|undefined변수를 갖는 객체이다
- datapageParam(getNextPageParam을 사용시)을 리턴해야 한다.
getNextPageParam (lastPage, allPages) => unknown | undefined - 쿼리의 신규데이터를 받으면 getNextPageParam 함수에 infinite data list의 마지막 페이지와 페이지들의 full array가 인자로 전달된다
- single variable을 리턴해야 한다. 이 단일 변수는 쿼리함수의 마지막 옵셔널 파라미터에 전달된다.
- undefined를 리턴시키면 다음페이지가 없음을 가리킨다.
getPreviousPageParam (firstPage, allPages) => unknown | undefined - 쿼리의 신규데이터를 받으면 getPreviousParam 함수에 infinite data list의 첫 페이지와 모든 페이지가 인자로 전달된다.
- single variable 을 리턴해야 되는데, 이 단일 변수는 쿼리의 마지막 옵셔널 파라미터에 전달된다.
-undefined를 리턴면 이전 페이지가 없다는 걸 가리킨다.

리턴값

  • useInfiniteQuery 의 리턴은 useQuery 훅이랑 동일한데, 아래 내용이 추가된다.
옵션명 타입 default 설명
data.pages TData[] 모든 페이지를 담은 배열
data.pageParams unknown[] 모든 page params를 담은 배열
isFetchingNextPage boolean fetchNextPage로 다음 페이지가 패칭되는 동안 true
isFetchingPreviousPage boolean fetchPreviousPage로 다음 페이지가 패칭되는 동안 true
fetchNextPage (options?: FetchNextPageOptions) => Promise true 이 함수는 results의 다음 page를 패치하게 해준다.
options.pageParam: unknown는 특정한 page param을 사용해서 패칭하게 해준다.
options.cancelRefetch: booleantruefetchNextPage를 반복 호출하면 fetchPage도 매번 호출되고 또한 이전의 호출은 무시된다. falsefetchNextPage를 반복호출해도 첫번째 호출이 resolved가 되기전까지 기다린다.
fetchPreviousPage (options?: FetchPreviousPageOptions) => Promise 위와 비슷
hasNextPage boolean next page를 fetch할게 있으면 가 된다.(getNextPageParam옵션을 통해 정해짐)
hasPreviousPage boolean 위와 비슷

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions