기존 데이터 세트에 데이터를 추가적으로 "더 로드"하거나 "무한 스크롤"할 수 있는 리스트를 렌더링하는 것도 매우 일반적인 UI 패턴입니다. TanStack Query는 이러한 타입의 리스트들을 쿼리하는 데 유용한 useInfiniteQuery
라는 useQuery
버전을 지원합니다.
useInfiniteQuery
를 사용하면 몇 가지 달라진 점을 확인할 수 있습니다:
data
는 이제 무한 쿼리 데이터를 포함하는 객체입니다:data.
pages 배열data.pageParams
배열fetchNextPage
, fetchPreviousPage
함수를 사용할 수 있습니다.getNextPageParam
, getPreviousPageParam
옵션은 로드할 데이터가 더 있는지 여부와 fetch할 정보를 결정하는 데 모두 사용할 수 있습니다. 이 정보는 쿼리 함수에 추가 파라미터로 제공됩니다(선택적으로 fetchNextPage
또는 fetchPreviousPage
함수를 호출할 때 override 할 수 있음).hasNextPage
boolean을 사용할 수 있으며 getNextPageParam
이 undefined
이외의 값을 리턴하면 true
가 됩니다.hasPreviousPage
boolean을 사용할 수 있으며 getPreviousPageParam
이 undefined
이외의 값을 리턴하면 true
가 됩니다.isFetchingNextPage
, isFetchingPreviousPage
boolean을 사용할 수 있습니다."참고: 쿼리에서
initialData
또는select
와 같은 옵션을 사용하는 경우 데이터를 재구성할 때data.pages
,data.pageParams
속성이 여전히 포함되어 있는지 확인해야 합니다. 그렇지 않으면 쿼리 반환 시 변경 사항을 덮어쓰게 됩니다!"
다음 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 되어야 하는 경우, 각 그룹은 첫 번째 그룹부터 sequentially
fetched 됩니다. 이렇게 하면 기초 데이터가 변경되더라도 오래된 커서를 사용하여 레코드가 중복되거나 건너뛰는 일이 발생하지 않습니다. 무한 쿼리의 결과가 쿼리 캐시에서 제거되는 경우 pagination은 초기 상태에서 다시 시작되며 초기 그룹만 요청됩니다.
모든 페이지의 하위 집합만 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 됩니다.
기본적으로 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 })
}
양방향 목록은 getPreviousPageParam
, fetchPreviousPage
, hasPreviousPage
및 isFetchingPreviousPage
속성 및 함수를 사용하여 구현할 수 있습니다.
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
때로는 페이지를 역순으로 표시하고 싶을 수도 있습니다. 이 경우 select
옵션을 사용할 수 있습니다:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
첫 페이지를 수동으로 제거합니다:
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