Become a GraphQL expert

We're launching a brand new course. Pre-sale is now live.

View course

Overstacked

Tue Jul 23 2024

Handle pagination with urql

How to handle pagination with the urql GraphQL client

cover image

Popular GraphQL clients like Apollo-client have more in-built support for pagination through the fetchMore method. But with urql we need to handle pagination manually.

Although this might seem like a disadvantage, it gives us more control over how we handle pagination, and it's still quite simple to implement.

Let's say we have a simple GraphQL query which fetches a list of posts.

query Posts($limit: Int!, $offset: Int!) {
  posts(limit: $limit, offset: $offset) {
    id
    title
    body
  }
}

In Apollo-client we would use the fetchMore method to fetch more posts.

Apollo-client example:

const PostList = () => {
  const limit = 10
  const { data, fetchMore } = useQuery(POSTS_QUERY, {
    variables: { limit, offset: 0 },
  })

  const fetchMorePosts = () => {
    fetchMore({
      variables: { limit, offset: data.posts.length },
    })
  }

  return (
    <div>
      {data.posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
      <button onClick={fetchMorePosts}>Load more</button>
    </div>
  )
}

With urql there is no fetchMore, so we can write a hook to abstract the pagination logic.

urql example:

const PostList = () => {
  const limit = 10
  const { offset, fetchMore, hasMore, results, watch } = usePagination(limit)

  const [res] = useQuery(POSTS_QUERY, {
    variables: { limit, offset },
  })

  watch(res.data.posts)

  return (
    <div>
      {results.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
      <button onClick={fetchMore}>Load more</button>
    </div>
  )
}

Here we use a custom hook usePagination to handle the pagination for us. We can still use useQuery in pretty much the same way, except we pass the offset and limit to the query which is controlled by the usePagination hook.

usePagination hook with offset and limit:

const usePagination = <T extends {}>(limit: number) => {
  // We track the previous page of data to help us determine
  // 1. if the data has changed
  // 2. if we should fetch more data
  const [previousData, setPreviousData] = useState([] as T[])

  // We track the current offset
  const [offset, setOffset] = useState(0)

  // We store all the results in one array
  // As more data is fetched we append it to this array
  const [results, setResults] = useState<T[]>([])

  // We use watch to check if the data has changed.
  // When we get new data we append it to the results array
  const watch = (data?: T[]) => {
    if (!data) {
      return
    }
    if (previousData === data) {
      return
    }

    setPreviousData(data)
    setResults((prevResults) => [...prevResults, ...data])
  }

  // To fetch more data we simply update the offset
  // with the current length of the results array.
  // This will fetch the next page of data.
  const fetchMore = () => {
    setOffset(results.length)
  }

  return {
    watch,
    offset,
    fetchMore,
    results,
    hasMore: previousData.length >= limit,
  }
}

With this hook we can easily handle pagination with urql and have more control over how we fetch and display data.

For example instead of offset and limit you may prefer page and pageSize. You can easily change this in the usePagination hook.

usePagination hook with page and pageSize:

const usePagination = <T extends {}>(pageSize: number) => {
  const [previousData, setPreviousData] = useState([] as T[])

  const [page, setPage] = useState(1)
  const [results, setResults] = useState<T[]>([])

  const watch = (data?: T[]) => {
    if (!data) {
      return
    }
    if (previousData === data) {
      return
    }

    setPreviousData(data)
    setResults((prevResults) => [...prevResults, ...data])
  }

  return {
    watch,
    page,
    fetchMore: () => setPage(page + 1),
    results,
    hasMore: previousData.length >= pageSize,
  }
}

This hook can then be used with any query that requires pagination. Just save it in a separate file and import it wherever you need it.

import usePagination from './usePagination'

You can get more actionable ideas in my popular email newsletter. Each week, I share deep dives like this, plus the latest product updates. Join over 80,000 developers using my products and tools. Enter your email and don't miss an update.

You'll stay in the loop with my latest updates. Unsubscribe at any time.

© Copyright 2024 Overstacked. All rights reserved.

Created by Warren Day