
공식문서 : https://tanstack.com/query/v4/docs/guides/infinite-queries
기존 데이터 집합에 더 많은 데이터를 추가로 로드하거나 "무한 스크롤"할 수 있는 렌더링 리스트도 매우 일반적인 UI 패턴이다. React Query는 이러한 유형의 리스트들을 쿼리하기 위한 useInfiniteQuery라는 매우 유용한 버전의 useQuery를 지원한다.
useInfiniteQuery 사용시 몇가지 차이점이 있다 :
이제 data가 무한 쿼리 데이터를 포함하고 있는 객체이다.
data.pages fetch한 페이지들을 포함하는 배열
data.pageParams 페이지를 fetch하는데 사용되는 페이지 파라미터들을 포함하는 배열
이제 fetchNextPage 및 fetchPreviousPage 함수들을 사용할 수 있다.
getNextPageParam 및 getPreviousPageParam 옵션을 사용하여 로드할 데이터가 더 있는지와 fetch할 정보가 있는지 확인할 수 있다. 이 정보는 쿼리 함수에서 추가 파라미터로 제공된다(fetchNextPage 또는 fetchPreviousPage 함수를 호출할때 선택적으로 재정의할 수 있음)
이제 hasNextPage boolean을 사용할 수 있으며 getNextPageParam이 undefined 외의 값을 반환하면 true.
이제 hasPreviousPage boolean을 사용할 수 있으며 getPreviousPageParam이 undefined 이외의 값을 반환하면 true
isFetchingNextPage 및 isFetchingPreviousPage boolean들을 사용하여 백그라운드 refresh 상태와 추가 loading 상태를 구분할 수 있다.
참고 : 쿼리에서
initialData또는select같은 옵션을 사용할 때, 데이터를 재구성할때 여전히data.pages및data.pageParams속성을 포함하는지 확인해라. 그렇지 않으면 반환되는 쿼리에서 변경사항을 덮어쓰게 된다!
다음 projects 그룹을 fetch하는데 사용할 수 있는 커서와 함께 커서 인덱스를 기반으로 projects의 페이지 3개를 한번에 반환하는 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를 호출하지 않는것이 매우 중요하다. 예를들어, 이렇게 하지마라 :<button onClick={fetchNextPage} />. 이는 onClick 이벤트를fetchNextPage함수로 보내기 때문
import { useInfiniteQuery } from '@tanstack/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>
</>
)
}
infinite 쿼리가 stale하게 되고 refetch해야하는 경우, 각 그룹은 첫번째 그룹부터 시작하여 sequentially fetch된다. 이렇게 하면 기본 데이터가 변경되더라도 stale한 커서를 사용하지 않고 잠재적으로 중복을 가져오거나 레코드를 건너뛰지 않게 한다. queryCache에서 infinite 쿼리의 결과가 제거되면, pagination은 초기 그룹만 요청된 초기 state에서 재시작된다.
모든 페이지의 하위집합만 refetch 하고싶다면, refetchPage 함수를 useInfiniteQuery에서 반환된 refetch로 전달할 수 있다.
const { refetch } = useInfiniteQuery(['projects'], 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를 반환하는 페이지만 refetch된다.
기본적으로 getNextPageParam에서 반환된 변수는 쿼리 함수에 제공되지만, 경우에 따라 이를 재정의할 수 있다. 다음과 같이 기본 변수를 재정의하는 fetchNextPage 함수에 커스텀 변수를 전달할 수 있다.
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
status,
data,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// Pass your own page param
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}
양방향 list는 getPreviousPageParam, fetchPreviousPage, hasPreviousPage 및 isFetchingPreviousPage 속성 및 함수를 사용하여 구현할 수 있다.
useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
때로는 페이지를 역순으로 표시하고 싶을 수 있다. 이 경우 select 옵션을 사용할 수 있다 :
useInfiniteQuery(['projects'], 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,
}))
pages와 pageParams의 데이터 구조를 동일하게 유지해야한다!