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'