공식문서 : 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의 데이터 구조를 동일하게 유지해야한다!