react-query paginated infinite queries

반응형

 

번역: 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 의 데이터 구조와 같도록 유지해야 합니다.

 

반응형

댓글

Designed by JB FACTORY