Offset-based pagination
We recommend reading
With offset-based pagination, a list field accepts an offset
argument that indicates where in the list the server should start when returning items for a particular query. The field usually also accepts a limit
argument that indicates the maximum number of items to return:
type Query {feed(offset: Int, limit: Int): [FeedItem!]}type FeedItem {id: ID!message: String!}
This pagination strategy works well for immutable lists, or for lists where each item's index never changes. In other cases, you should avoid it in favor of
Although it has limitations, offset-based pagination is a common pattern in many applications, in part because it's relatively straightforward to implement.
The offsetLimitPagination
helper
offsetLimitPagination
helperApollo Client provides an offsetLimitPagination
helper function that you can use to generate a
This example uses offsetLimitPagination
to generate a field policy for Query.feed
:
import { InMemoryCache } from "@apollo/client";import { offsetLimitPagination } from "@apollo/client/utilities";const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: offsetLimitPagination()},},},});
This defines a merge
function
Using with fetchMore
fetchMore
If you use offsetLimitPagination
to set your feed policy as shown above, then you can use fetchMore
with useQuery
like so:
const FeedData() {const { loading, data, fetchMore } = useQuery(FEED_QUERY, {variables: {offset: 0,limit: 10},});// If you want your component to rerender with loading:true whenever// fetchMore is called, add notifyOnNetworkStatusChange:true to the// options you pass to useQuery above.if (loading) return <Loading/>;return (<Feedentries={data.feed || []}onLoadMore={() => fetchMore({variables: {offset: data.feed.length},})}/>);}
By default, fetchMore
uses the original query and variables
, so we only need to pass the variable that's changing: offset
. When new data is returned from the server, it's automatically merged with any existing Query.feed
data in the cache. This causes useQuery
to rerender with the expanded list of data.
In this example, the Feed
component receives the entire cached list (data.feed
) every time it renders, which includes data from all pages received so far. This is a read
function
Using with a paginated read
function
read
functionIn read
function
Because the offsetLimitPagination
helper is currently defining your field policy, you combine your read
function with the helper's result, like so:
import { InMemoryCache } from "@apollo/client";import { offsetLimitPagination } from "@apollo/client/utilities";const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {...offsetLimitPagination(),read(existing, { args }) {// Implement here}}},},},});
For example implementations, see read
functions
If you use a paginated read
function, you probably need to update your offset
and limit
variables as required by your use case after you call fetchMore
. Otherwise, you'll continue rendering only the first page of results.
For example, to display all the data received so far, you could modify the previous example as follows:
const FeedData = () => {const [limit, setLimit] = useState(10);const { loading, data, fetchMore } = useQuery(FEED_QUERY, {variables: {offset: 0,limit,},});if (loading) return <Loading/>;return (<Feedentries={data.feed || []}onLoadMore={() => {const currentLength = data.feed.length;fetchMore({variables: {offset: currentLength,limit: 10,},}).then(fetchMoreResult => {// Update variables.limit for the original query to include// the newly added feed items.setLimit(currentLength + fetchMoreResult.data.feed.length);});}}/>);}
This code uses a React useState
Hook to store the current limit
value, which it updates by calling setLimit
in a callback attached to the Promise
returned by fetchMore
.
You could store offset
in a React useState
Hook as well, if you need the offset
to change. Exactly when and how these variables
change is up to your component, and may not always be the result of calling fetchMore
, so it makes sense to use React component state to store these variable values.
If you are not using React and useQuery
, the ObservableQuery
object returned by client.watchQuery
has a method called setVariables
that you can call to update the original variables.
Because fetchMore
requires some extra work to update the original variables if you're using a read
function that is sensitive to those variables (the second kind of read
function), it's fair to say fetchMore
encourages the first kind of read
function, which simply returns all available data.
However, now that you understand your options, there's nothing wrong with moving read-time pagination logic out of your application code and into your field read
functions. Both kinds of read
functions have their uses, and both can be made to work with fetchMore
.
Setting keyArgs
with offsetLimitPagination
keyArgs
with offsetLimitPagination
If a paginated field accepts arguments besides offset
and limit
, you might need to
To set keyArgs
for the field policy generated by offsetLimitPagination
, provide an array of argument names to the function as a parameter:
fields {// Results belong to the same list only if both the type// and userId arguments match exactlyfeed: offsetLimitPagination(["type", "userId"])}
By default, offsetLimitPagination
uses keyArgs: false
(no key arguments).