react-query paginated infinite queries
- REACT & NODE
- 2021. 11. 15.
번역: https://react-query.tanstack.com/guides/paginated-queries
Paginated/Lagged Queries
페이지네이션 데이타를 렌더링하는 것은 리액트 쿼리에서 매우 일반적인 UI 패턴입니다. 단지 페이지 정보만을 포함하면 작동합니다.
const result = useQuery(['projects', page], fetchProjects)
하지만, 이 예제를 실행하면 이상한 걸 알 수 있습니다: success / loading 상태를 들락날락거립니다. 왜냐하면 각 새로운 페이지는 새로운 쿼리로 취급되기 때문입니다.
이러한 부분은 적절하지 않습니다. 그리고 운 나쁘게도 많은 툴들이 이렇게 동작합니다. 하지만 리액트 쿼리는 아닙니다! 예상하듯이, 리액트 쿼리는 이것을 다룰 수 있는 keepPreviousData 라는 멋진 기능을 가지고 있습니다.
keepPreviousData 를 사용한 더 나은 페이지네이션 쿼리
쿼리를 위해 페이지 인덱스 또는 커서를 증가시키기 위한 다음의 예제를 살펴보자. useQuery 를 사용했다면, 여전히 기술적으로 잘 동작한다. 하지만 각 페이지나 커서를 위해 생성, 제거가 되어지는 다른 쿼리로서 success / loading 상태를 왔다갔다할 것이다. keepPreviousDate 를 true 로 셋팅함으로써 새로운 몇가지를 얻을 수 있다:
- 비록 query key 가 바뀌더라도 새로운 데이터가 요청되어지는 동안 마지막으로 성공했던 fetch 로 부터의 데이터가 가능하다.
- 새로운 데이터가 도착하면, 기존 데이터는 새로운 데이터를 보여주기 위해서 끊김없이 교체된다.
- isPreviousData 는 해당 쿼리가 현재 제공하고 있는 것이 어떤 데이터인지를 알게 해준다.
function Todos() {
const [page, setPage] = React.useState(0)
const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json())
const {
isLoading,
isError,
error,
data,
isFetching,
isPreviousData,
} = useQuery(['projects', page], () => fetchProjects(page), { keepPreviousData : true })
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : isError ? (
<div>Error: {error.message}</div>
) : (
<div>
{data.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</div>
)}
<span>Current Page: {page + 1}</span>
<button
onClick={() => setPage(old => Math.max(old - 1, 0))}
disabled={page === 0}
>
Previous Page
</button>{' '}
<button
onClick={() => {
if (!isPreviousData && data.hasMore) {
setPage(old => old + 1)
}
}}
// Disable the Next Page button until we know a next page is available
disabled={isPreviousData || !data?.hasMore}
>
Next Page
</button>
{isFetching ? <span> Loading...</span> : null}{' '}
</div>
)
}
keepPreviousData 를 사용한 Lagging Infinite 쿼리 결과
일반적이진 않지만, keepPreviousData 옵션은 또한 useInfiniteQuery 훅과 잘 동작한다. 그래서 infinite query key 가 시간에 따라 변해도 사용자가 캐시된 데이터를 계속적으로 끊이없이 볼 수 있도록 할 수 있다.
번역: https://react-query.tanstack.com/guides/infinite-queries
Infinite Queries
기존 데이터에 데이터를 추가적으로 불러오는 또는 무한 스크롤의 리스트를 렌더링하는 것 또한 매우 일반적인 UI 패턴입니다. 리액트 쿼리는 useInfiniteQuery 라 불리는 이런 종류의 리스트를 쿼리하기 위한 새로운 useQuery 버전을 제공합니다.
useInfiniteQuery 를 사용할 때, 다음과 같은 몇가지 다른점을 알 수 있습니다.
- data 는 이제 infinite query data 를 담고 있는 객체입니다.
- data.pages 배열은 fetch 된 페이지들을 포함합니다.
- data.pageParams 배열은 페이지를 fetch 하기 위해 사용된 페이지 파라미터들을 포함합니다.
- fetchNextPage 와 fetchPreviousPage 함수를 이제 사용할 수 있습니다.
- getNextPageParam 과 getPreviousPageParam 옵션은 읽어올 데이터가 더 있을 경우 와 해당 정보를 fetch 하려고 할 경우 가능합니다. 그 정보는 쿼리 함수에서 추가적인 파라미터로 제공됩니다. (fetchNextPage 나 fetchPreviousPage 함수를 호출할 때 옵션으로 재정의 될 수 있습니다.)
- hasNextPage 불린값을 이제 사용가능합니다. getNextPageParam 이 undefined 말고 값을 리턴할 경우 true 가 됩니다.
- hasPreviousPage 불린값을 이제 사용가능합니다. getPreviousPageParam 이 undefined 말고 값을 리턴할 경우 true 가 됩니다.
- isFetchingNextPage 와 isFetchingPreviousPage 불린값을 이제 사용가능합니다. 백그라운드 상태 리프레쉬 와 추가적인 상태를 로딩하는 것을 구분할 때 사용됩니다.
initialData 나 select 같은 옵션들을 쿼리에서 사용할 때, 데이터를 재구성하면서 data.pages 와 data.pageParams 를 여전히 포함하고 있는지를 확인해야합니다. 그렇지 않을 경우 그 변경은 그 리턴되는 쿼리에 의해서 덮어 써질 것입니다.
Example
다음 projects 의 그룹을 fetch 하기위해 사용되어질 수 있는 cursor index 가 있고 그에 따라 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 데이터를 재정의하기를 원하지 않는다면 arguments 와 함께fetchNextParam 을 호출하지 않아야하는 것은 매우 중요합니다. 예를 들어 이런 것을 하면 안됩니다. <button onClick={fetchNextPage} /> 이것은 fetchNextPage 함수로 onClick event 를 전달할 것이기 때문입니다.
import { useInfiniteQuery } from '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 query 가 refetch 되어질 필요가 잇을 때 어떤 일이 일어날까?
infinite query 가 stale 이 되고 refetch 될 필요가 있을 때 각 그룹은 처음 것부터 시작해서 순차적으로 fetch 됩니다. 이것은 관련 데이터들이 변경되었어도 그것을 보장해주고, stale cursor 들을 사용하지 않고 잠재적으로 중복을 받거나 레코드를 스킵하는 것을 사용하지 않습니다. infinite query 의 결과들이 queryCache 로부터 제거되었더라도 페이지네이션은 최초의 그룹부터 처음 상태에서 재시작합니다.
refetchPage
능동적으로 모든 페이지 중의 일부를 refetch 하길 원한다면, useInfiniteQuery 로 부터 리턴된 refetch 함수에 refetchPage 함수를 전달할 수 있습니다.
const { refetch } = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })
또한 queryClient.refetchQueries, queryClient.invalidateQueries, queryClient.resetQueries 에 두번째 argument (queryFilters) 의 부분으로 이 함수를 전달할 수 있습니다.
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 })
}
만약 양방향 infinite 리스트를 구현하고 싶다면?
양방향 리스트는 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(),
}),
})
만약 infinite 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,
}))
pages 와 pageParams 의 데이터 구조와 같도록 유지해야 합니다.
'REACT & NODE' 카테고리의 다른 글
React 는 왜 class 보다 function hook 을 쓰는가? (0) | 2022.06.26 |
---|---|
React 에러 처리, Boundaries 그리고 포인트 (0) | 2021.12.02 |
react-query useInfiniteQuery (0) | 2021.10.05 |
mvc flux mvvm redux constate recoil jotai sagen ... (0) | 2021.09.27 |
postcss autoprefixer CRA CNA reset styled-components (0) | 2021.09.25 |