[TanStakQuery] Infinite Queries

Jeris·2023년 5월 22일
0

기존 데이터 세트에 데이터를 추가적으로 "더 로드"하거나 "무한 스크롤"할 수 있는 리스트를 렌더링하는 것도 매우 일반적인 UI 패턴입니다. TanStack Query는 이러한 타입의 리스트들을 쿼리하는 데 유용한 useInfiniteQuery라는 useQuery 버전을 지원합니다.

useInfiniteQuery를 사용하면 몇 가지 달라진 점을 확인할 수 있습니다:

  • data는 이제 무한 쿼리 데이터를 포함하는 객체입니다:
  • fetched 페이지를 포함하는 data.pages 배열
  • 페이지를 fetch하는 데 사용된 페이지 파라미터를 포함하는 data.pageParams 배열
  • 이제 fetchNextPage, fetchPreviousPage 함수를 사용할 수 있습니다.
  • getNextPageParam, getPreviousPageParam 옵션은 로드할 데이터가 더 있는지 여부와 fetch할 정보를 결정하는 데 모두 사용할 수 있습니다. 이 정보는 쿼리 함수에 추가 파라미터로 제공됩니다(선택적으로 fetchNextPage 또는 fetchPreviousPage 함수를 호출할 때 override 할 수 있음).
  • 이제 hasNextPage boolean을 사용할 수 있으며 getNextPageParamundefined 이외의 값을 리턴하면 true가 됩니다.
  • 이제 hasPreviousPage boolean을 사용할 수 있으며 getPreviousPageParamundefined 이외의 값을 리턴하면 true가 됩니다.
  • 이제 백그라운드 새로 고침 상태와 더 로딩 중 상태를 구분하기 위해 isFetchingNextPage, isFetchingPreviousPage boolean을 사용할 수 있습니다.

"참고: 쿼리에서 initialData 또는 select와 같은 옵션을 사용하는 경우 데이터를 재구성할 때 data.pages, data.pageParams 속성이 여전히 포함되어 있는지 확인해야 합니다. 그렇지 않으면 쿼리 반환 시 변경 사항을 덮어쓰게 됩니다!"

Example

다음 projects 그룹을 fetch하는 데 사용할 수 있는 커서와 함께, cursor 인덱스를 기반으로 한 번에 3개씩 projects 페이지를 리턴하는 API가 있다고 가정해 보겠습니다:

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" UI를 만들 수 있습니다:

  • 기본적으로 첫 번째 데이터 그룹을 요청하기 위해 useInfiniteQuery를 기다립니다.
  • 다음 쿼리에 대한 정보를 getNextPageParam에 리턴합니다.
  • fetchNextPage 함수 호출

"참고: getNextPageParam 함수에서 리턴된 pageParam 데이터를 재정의하지 않으려면 아규먼트를 사용하여 fetchNextPage를 호출하지 않는 것이 매우 중요합니다. 예를 들어, 이렇게 하면 onClick 이벤트가 fetchNextPage 함수로 전송되기 때문에 <button onClick={fetchNextPage} /> 이런 식으로 작성하지 마십시오."

import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam = 0 }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: 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.data.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>
    </>
  )
}

무한 쿼리가 refetched 되어야 하는 경우 어떻게 하나요?

무한 쿼리가 오래되어 refetched 되어야 하는 경우, 각 그룹은 첫 번째 그룹부터 sequentially fetched 됩니다. 이렇게 하면 기초 데이터가 변경되더라도 오래된 커서를 사용하여 레코드가 중복되거나 건너뛰는 일이 발생하지 않습니다. 무한 쿼리의 결과가 쿼리 캐시에서 제거되는 경우 pagination은 초기 상태에서 다시 시작되며 초기 그룹만 요청됩니다.

refetchPage

모든 페이지의 하위 집합만 refetch하려는 경우 refetchPage 함수를 전달하여 useInfiniteQuery에서 리턴된 페이지를 refetch할 수 있습니다.

const { refetch } = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })

이 함수를 두 번째 아규먼트(queryFilters)의 일부로 queryClient.refetchQueries, queryClient.invalidateQueries 또는 queryClient.resetQueries에 전달할 수도 있습니다.

Signature

  • refetchPage: (page: TData, index: number, allPages: TData[]) => boolean

이 함수는 각 페이지마다 실행되며, 이 함수가 true를 리턴하는 페이지만 다시 fetched 됩니다.

쿼리 함수에 커스텀 정보를 전달해야 하는 경우 어떻게 하나요?(What if I need to pass custom information to my query function?)

기본적으로 getNextPageParam에서 리턴된 변수가 쿼리 함수에 제공되지만 경우에 따라 이를 override하고 싶을 수 있습니다. 다음과 같이 기본 변수를 override하는 커스텀 변수를 fetchNextPage 함수에 전달할 수 있습니다:

function Projects() {
  const fetchProjects = ({ pageParam = 0 }) =>
    fetch('/api/projects?cursor=' + pageParam)

  const {
    status,
    data,
    isFetching,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  // Pass your own page param
  const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}

양방향 무한 리스트를 구현하려면 어떻게 해야 하나요?(What if I want to implement a bi-directional infinite list?)

양방향 목록은 getPreviousPageParam, fetchPreviousPage, hasPreviousPageisFetchingPreviousPage 속성 및 함수를 사용하여 구현할 수 있습니다.

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})

페이지를 역순으로 표시하려면 어떻게 해야 하나요?(What if I want to show the pages in reversed order?)

때로는 페이지를 역순으로 표시하고 싶을 수도 있습니다. 이 경우 select 옵션을 사용할 수 있습니다:

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  select: (data) => ({
    pages: [...data.pages].reverse(),
    pageParams: [...data.pageParams].reverse(),
  }),
})

무한 쿼리를 수동으로 업데이트하려면 어떻게 해야 하나요?(What if I want to manually update the infinte query?)

첫 페이지를 수동으로 제거합니다:

queryClient.setQueryData(['projects'], (data) => ({
  pages: data.pages.slice(1),
  pageParams: data.pageParams.slice(1),
}))

개별 페이지에서 단일 값을 수동으로 제거합니다:

const newPagesArray =
  oldPagesArray?.pages.map((page) =>
    page.filter((val) => val.id !== updatedId),
  ) ?? []

queryClient.setQueryData(['projects'], (data) => ({
  pages: newPagesArray,
  pageParams: data.pageParams,
}))

페이지와 페이지 파라미터의 데이터 구조를 동일하게 유지해야 합니다!

Reference

profile
job's done

0개의 댓글