When using pagination and doing a fetchMore request the cache is not used to read data,
While data is already in the cache and having implementations for 'read' and 'merge' field policies, it looks like the 'fetchMore' is not using the typePolicy 'read' function and direcly does a network request.
Also the fetchPolicy is not used, for example if you set it to 'cache-only', fetchmore is doing network requests.
It's kinda like the example from the documentation:
const FeedData({ type = "PUBLIC" }) {
const [limit, setLimit] = useState(10);
const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
variables: {
type: type.toUpperCase(),
offset: 0,
limit,
},
});
if (loading) return <Loading/>;
return (
<Feed
entries={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);
});
}
/>
);
}
Sometimes if you scroll 500 items with a continuous scroll implementation, which loads 500 items in the cache, the component gets unmounted, and mounts again, you do not want all the 500 items to show at once because the rendering could be slow. I want to just to show the initial 10 items, and "fetchMore" from the cache or network (if it's not in the cache), while you scroll for more items.
so in the typePolicy I have something like:
const policy = {
Query: {
fields: {
feed: {
keyArgs: ['type'],
merge: {
// merge from offsetLimitPagination
},
read(existing, { args }) {
if (!args || !existing || !(args.limit >= 0)) {
return existing;
}
if (existing.length >= args.limit + (args.offset ?? 0)) {
return existing.slice(args.offset ?? 0, args.limit ?? existing.length);
}
// not enough data
},
},
},
},
};
But 'read' is never called when using fetchmore and always does a network call even though all the data is in the cache for the first 500 items.
Apollo version 3.3
@robertsmit Can you share the code for your field policy (including read, merge, and keyArgs) and fetchMore call?
@robertsmit Can you share the code for your field policy (including
read,merge, andkeyArgs) andfetchMorecall?
I have added some code for clearance.
@robertsmit If you have some way to detect the event of navigating back to the component, you should be able to call setLimit(10) to reset the window.
The fetchMore method sends a separate request that always has a fetch policy of no-cache, which is why it doesn't try to read from the cache first.
@benjamn I have a related question about offsetLimitPagination. We're implementing an infinite scrolling list where mutations can happen on items in the list (think: editing a comment in a news feed). Using offsetLimitPagination helps tremendously with reactively updating the UI after mutations, and automating the feed concatenation, but it feels super hard to maintain.
For example:
typePolicies: {
Query: {
fields: {
posts: offsetLimitPagination([/* this array has to contain every possible argument except for offset? otherwise it breaks "post" queries that aren't supposed to infinite scroll */])
}
}
}
To be a little more concrete: when I was forgetting a particular key, fetching the next page of the feed would cause other pieces of UI to concatenate results where I wouldn't want them to (i.e. a section with a limit of 6 items is next to a feed, when the feed calls "fetchMore" now all of a sudden both areas get the next page of data unless I backtrack and add all relevant keyArgs for both queries).
So if I want an infinite scrolling list, now I have to add keyArgs for every other combination of arguments for posts queries throughout our repo? It would be great if I could use this typePolicy on a specific instance of useQuery.. or if this global one had an option to just consolidate anything with offset (kind of like the inverse of how I think it currently works). Maybe I'm misunderstanding something here altogether though.
Edit: I think someone else encountered this here for what that's worth.
@benjamn
With navigating away, I mean the component gets unmounted (in case of react). So when you mount the component again, the initial limit is 10, we have 500 items in het cache, but there is no possible solution to use fetchmore to load more items from the cache. Initial loading 500 items at once is not what you want, it can be slow.
Because there is an explicit read typepolicy which respects the page arguments, it would be nice if I could force fetchMore to read in the cache first and then, if there is nothing in the cache, do the network-request, as in 'cache-first'. Because there is an explicit read typePolicy this works.
So maybe fetchMore has an implicit fetchPolicy 'network-only', what if I set it directly from the call side to 'cache-first', this could work right?
const FeedData({ type = "PUBLIC" }) {
const [limit, setLimit] = useState(10);
const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
variables: {
type: type.toUpperCase(),
offset: 0,
limit,
},
});
if (loading) return <Loading/>;
return (
<Feed
entries={data.feed || []}
onLoadMore={() => {
const currentLength = data.feed.length;
fetchMore({
fetchPolicy: 'cache-first',
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);
});
}
/>
);
}
@awlevin I had also this problem. My current solution is to generate some typePolicies based on the schema. New arguments to fields are generated in the type policies automatically. But offcourse, a negated keyArgs would be nice. Something like excludedKeysArgs. Then the field key should be constructed with argnames included and sorted by argname.
@robertsmit I might call it nonKeyArgs for brevity, but I like the idea! Your other idea about allowing fetchMore to take a non-default options.fetchPolicy (rather than always using no-cache) is interesting too.
Though I'm still open to something like nonKeyArgs, I realized after writing my previous comment that you can currently provide a custom keyArgs function to implement whatever behavior you want:
new InMemoryCache({
typePolicies: {
SomeType: {
fields: {
someField: {
keyArgs(args) {
// return a string based on args
},
},
},
},
},
})
@benjamn Yes I do this on some places but take care to make it deterministic. For example first order de args by name and then serialize.
Most helpful comment
@awlevin I had also this problem. My current solution is to generate some typePolicies based on the schema. New arguments to fields are generated in the type policies automatically. But offcourse, a negated keyArgs would be nice. Something like excludedKeysArgs. Then the field key should be constructed with argnames included and sorted by argname.