Use cached pagination data if query has already been requested before?

I’m using using Apollos useQuery hook in react to make a paginated request to my graphql server. The component provides set filters that can be toggled by the user and simply will add the value to the variables property.

My Issue

When a use scrolls through the component and fetches more paginated data, the query is correctly updated to append the new data onto the old data. The issue is, when a user fetches the initial query and subsequent pages (lets say each pages 20 items and the user has fetched 3 pages). If the user changes to a different filter and again changes back to the original filter, all the previous fetched paginated data will be overwritten and the user will have to re-fetch the paginated data they already again.

What I want

What I expect is, when a user loads data with a specific filter (including the fetchMore data) all the data should be stored in cache. Similarly, if the user switches filter, the data with the updated filter (inculding the fetchMore data) should be stored in cache. Then, if a request is made with a filter that has already been requested then it should pull all items (including extra paginated items) and return those items.

What Ive tried

  1. Ive tried using the nextFetchPolicy to use the network-only policy on first request then any request after use cache-first but this didnt seem to work as it would treat variable changes as the same query and always use the cache.
nextFetchPolicy: (currentFetchPolicy, { reason }) => {
  if (reason === 'variables-changed') {
    return 'network-only'
  }
  if (
    currentFetchPolicy === 'network-only' ||
    currentFetchPolicy === 'cache-and-network'
  ) {
    return 'cache-first'
  }
  return currentFetchPolicy
},
  1. Tried using typePolicies in the InMemoryCache class which seemed right up until it would do the exact same thing as it was doing without the typepolicy
new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        getSomeData: {
          keyArgs: ['filters'],
          merge(existing = {}, incoming, { args }) {
            if (!existing) {
              return incoming
            }

            if (args?.after || args?.before) {
              return {
                ...incoming,
                data: [...existing.data, ...incoming.data],
              }
            }

            return incoming
          },
        },
      },
    },
  },
})
  1. Ive not actually tried this approach but want to avoid it at all cost is to create custom caching solution but I know this will take longer and be riddled with edge cases

Schema


  // Used as we may want multiple completely unrelated filters
  input SelectedUserFilters {
    key: String
    selected: [String]
  }
  
  type Query {
    getFilters: [String]
    getSomeData(filters: [SelectedFilter], before: String, after: String, limit: Int): FeedPagination
  }
 
  type CursorPagination {
    next: String
    previous: String
    total: Int
    size: Int
    data: [SomeDataModel]!
  }

Any help would be great. Thanks you in advance.

Hi there!

I’m trying to understand the problem a bit more.

You are writing

If the user changes to a different filter and again changes back to the original filter, all the previous fetched paginated data will be overwritten and the user will have to re-fetch the paginated data they already again.

Assuming you have correct keyArgs and don’t force a new network request, switching back to old filters should immediately serve the previous pagination from the cache instead of retrieving new data from the cache.

Do you have a specific networkPolicy here that forces a refetch? In that case a new incoming result might overwrite the existing cache entry.

Only if that is the case, you might need a merge policy (I assume that up until then you were merging everything in updateQuery) - but that merge policy would likely need to be a lot more intricate. It seems like you are using cursor pagination, but not relay-style cursor pagination.

Is there a specific reason for that schema design? We do offer helpers for relay-style cursor pagination.

All that said, could you share a bit more of your component/useQuery code? That part is still missing for a complete picture.

Sorry for the delay,

I did find a solution but it required dynamically switching the fetch policy and storing a key to the variables used which I believe is what Apollo already does, I just can figure out how to work with it.

Without showing too much, this is basically everything in terms of handling the pagination. The useQuery doesnt have much else going for it other than a fetchPolicy: “cache-and-network".

// Helper function
const mergeApolloResult = <TData, TDataKey extends string = string>(
  dataKey: TDataKey,
  previous: ApolloResult<TData, TDataKey>,
  fetchMoreResult: ApolloResult<TData, TDataKey>,
): ApolloResult<TData, TDataKey> => {
  if (!fetchMoreResult) return previous
  const prevData = previous?.[dataKey]
  const moreData = fetchMoreResult?.[dataKey]
  if (Array.isArray(prevData) && Array.isArray(moreData)) {
    return { ...previous, [dataKey]: [...prevData, ...moreData] }
  }
  if (isPaginatedResult(prevData) && isPaginatedResult(moreData)) {
    return {
      ...previous,
      [dataKey]: {
        ...moreData,
        data: [...prevData.data, ...moreData.data],
      },
    }
  }
  return previous
}
// Triggered when the component wants to render the next page
const result = await props.fetchMore({
  updateQuery: (previous, { fetchMoreResult }) => {
    return mergeApolloResult(dataKey, previous, fetchMoreResult)
  },
  variables: {
    before: data.data[data.data.length - 1]?.id, // Get the last item in array
    pageSize: limit,
    filters: filters,
  },
})

I do have some concerns that I was applying keyArgs wrong.