Tanstack Query 공식문서의 Infinite Queries를 번역한 글입니다. 피드백은 댓글로 부탁드립니다.
기존 데이터에 더해 데이터를 "더 불러와서" 렌더링 하는 것이나 "무한 스크롤"은 매우 흔한 UI 패턴입니다. Tanstack Query는 이런 리스트형 데이터를 요청하기 위해, useQuery
의 유용한 버전인 useInfiniteQuery
를 지원합니다.
useInfiniteQuery
를 사용할 때, 몇 가지가 다르다는 걸 알아차릴 것입니다.
data
는 infinite query 데이터가 담겨있는 객체입니다.data.pages
은 fetch한 페이지들이 담겨있는 배열입니다.data.pageParams
은 페이지들을 fetch 하는 데에 필요한 page params가 담겨있는 배열입니다.fetchNextPage
와 getPreviousPageParam
를 사용할 수 있습니다.getNextPageParam
와 getPreviousPageParam
옵션은 불러올 데이터가 더 있는지 여부와 fetch할 정보를 결정할 때 사용할 수 있습니다. 이 정보는 query 함수에 추가 매개 변수로 제공됩니다.fetchNextPage
또는 fetchPreviPage
함수를 호출할 때, 선택적으로 오버라이드 될 수 있음)getNextPageParam
함수가 undefined
가 아닌 다른 값을 반환하면 hasNextPage
는 true
입니다.getPreviousPageParam
함수가 undefined
가 아닌 다른 값을 반환하면 hasPreviousPage
는 true
입니다.isFetchingNextPage
와 isFetchPreviousPage
로 백그라운드 새로고침 상태인지 추가 로딩 중인 상태인지 구별할 수 있습니다.주의: query에서
initialData
나select
같은 옵션을 사용할 경우, 데이터를 재구성할 때data.pages
와data.pageParams
속성이 계속 담겨있도록 해야 합니다. 그렇게 하지 않으면 query가 반환될 때 변경사항을 덮어쓰게 됩니다!
한 번의 요청에 projects
3개와 다음 projects
3개를 불러올 때 사용할 수 있는 cursor
를 불러오는 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: [...] }
이 정보를 가지고 우리는 "더 불러오기" UI를 이렇게 만들 수 있습니다.
useInfiniteQuery
의 default 동작인 첫번째 데이터 그룹 요청을 기다리기 getNextParam
으로 다음 요청을 위한 정보 반환하기fetchNextPage
함수 호출하기주의:
getNextParam
함수가 반환한pageParam
을 덮어쓰고 싶은 게 아니라면,fetchNextPage
에 인자를 넘겨서 호출하지 말아야 합니다.
예시)<button onClick={fetchNextPage} />
<= 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.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 query가 stale
상태가 되어서 refetch 되어야 하면, 각 데이터 그룹은 첫번째 그룹부터 순차적으로
fetch 됩니다. 이렇게 함으로써 서버의 데이터가 변형되더라도, 오래된 커서를 사용하지 않는다는 것과 중복 또는 생략된 데이터를 받지 않을 거란 걸 확실히 할 수 있습니다. 만약 infinite query의 결과가 queryCache에서 제거되면, 페이지네이션은 첫번재 데이터 그룹만 요청하는 초기 상태에서 재시작됩니다.
전체 페이지 중 일부만 직접 refetch 하고 싶을 때에는, useInfiniteQuery
가 반환하는 refetch
함수에 refetchPage
를 넘겨주면 됩니다.
const { refetch } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// 첫번째 페이지만 refetch 합니다.
refetch({ refetchPage: (page, index) => index === 0 })
이 함수를 queryClient.refetchQueries, queryClient.invalidateQueries, queryClient.resetQueries에 두번째 인자(queryFilters
)로 넘길 수도 있습니다.
refetchPage: (page: TData, index: number, allPages: TData[]) => boolean
이 함수는 각 페이지에 대해 실행되며, 이 함수가 true
를 반환하는 페이지만 refetch 됩니다.
getNextPageParam
이 반환한 변수들은 query 함수로 넘어가는 게 default지만, 그 변수들을 덮어쓰고 싶다면 아래와 같이 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,
})
// 여러분이 원하는 pageParam을 넘기세요.
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}
양방향 list는 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,
}))
pages
와 pageParams
가 각각 동일한 데이터 구조를 유지하도록 하세요!