Infinite Scroll Component with React Query
Published: 2023-01-08 04:17:44

[✨ **Watch on YouTube**](https://youtu.be/mZfDvfs2GtI)  |  [🐙 GitHub](https://github.com/RodionChachura/react-toolkit)  |  [🎮 Demo](https://reactkit.radzion.com/infinite-scroll)

Let's make an infinite scroll component with React Query. Here we have a simple table fetching new items on scroll.

![](scroll.png)

To query an API with paginated results, we are using the `useInfiniteQuery` hook that receives a key, query function, and configuration. To fake an API, we have the `queryItems` function that receives the startAt index and returns a list of items together with an index of the next item.

```tsx
const {
  data,
  isLoading,
  isFetchingNextPage,
  fetchNextPage,
  isFetched,
  isIdle,
} = useInfiniteQuery(
  "items",
  async ({ pageParam }) => {
    if (pageParam === null) return

    const result = await queryItems({ startAt: pageParam })

    return result
  },
  {
    refetchOnMount: true,
    keepPreviousData: false,
    getNextPageParam: (lastPage) => lastPage?.nextItem || null,
  }
)
```

The page param for the query function comes from the `getNextPageParam` that converts the last response to the param we need to query the next page. It is null when we are on the final page or the first request.

The data coming from the hook will have a format of pages, and to convert it to a plain list, we leverage the `usePaginatedResultItems`. It takes data and a function to extract items from the page result. Here we create an empty list, go through every page, and convert them to items.

```tsx
import { useMemo } from "react"
import { InfiniteData } from "react-query"

export function usePaginatedResultItems<T, V>(
  data: InfiniteData<T | undefined> | undefined,
  getPageItems: (page: T) => V[]
): V[] {
  return useMemo(() => {
    const items: V[] = []

    data?.pages?.forEach((page) => {
      if (page) {
        items.push(...getPageItems(page))
      }
    })

    return items
  }, [data?.pages, getPageItems])
}
```

To show the loader and request new items on scroll, we have the `PaginatedView` component. Here we display children together with the footer. To know if a user scrolled to the end of the list, we are leveraging the useIntersection hook that builds on top of Intersection Observer API. Below we have an effect that will request to load more items in case of intersection.

```tsx
import { ReactNode, useEffect, useRef } from "react"
import { useIntersection } from "react-use"
import styled from "styled-components"
import { Spinner } from "./Spinner"
import { HStack, VStack } from "./Stack"
import { Text } from "./Text"

interface Props {
  children: ReactNode
  isLoading?: boolean
  onRequestToLoadMore: () => void
}

const LoaderContainer = styled(HStack)`
  color: ${({ theme }) => theme.colors.textSupporting2.toCssValue()};
`

const Footer = styled(VStack)`
  grid-column: 1/-1;
`

export const PaginatedView = ({
  children,
  isLoading,
  onRequestToLoadMore,
}: Props) => {
  const ref = useRef<HTMLDivElement>(null)

  const intersection = useIntersection(ref, {
    root: null,
    rootMargin: "200px",
    threshold: 0,
  })

  useEffect(() => {
    if (intersection?.isIntersecting && !isLoading) {
      onRequestToLoadMore()
    }
  }, [intersection?.isIntersecting, onRequestToLoadMore, isLoading])

  return (
    <>
      {children}
      <Footer fullWidth alignItems="center">
        <div ref={ref} />
        {isLoading && (
          <LoaderContainer
            fullWidth
            gap={8}
            justifyContent="center"
            alignItems="center"
          >
            <Spinner size={16} />
            <Text>Loading</Text>
          </LoaderContainer>
        )}
      </Footer>
    </>
  )
}
```