Hi,
Given the situation :
...
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
people: {
keyArgs: false,
merge: (existing, incoming, { args }) => {
const offset = args?.skip || 0;
const merged = existing ? existing.slice(0) : [];
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i];
}
return merged;
}
}
}
}
}
}),
...
const { loading, data, fetchMore } = useQuery(ALL_PEOPLE, {
variables: { skip: 0 },
fetchPolicy: 'network-only'
});
<button onClick={() => fetchMore({ variables: { skip: data?.people.length } })}>
Next page
</button>
Bug : linked query is triggered twice
Here a sandbox : https://codesandbox.io/s/black-snowflake-vdhwm
There are some logs showing the linked query fired twice (as the bug description) on "Next Page" click. It doesn't happen if fetchPolicy is unset.
Thanks for reading.
@jgan42 I'm happy to say we now have documentation that covers this topic (see the setLimit example in particular)!
In short, the original useQuery query has its own variables, which are distinct from the variables passed to fetchMore, and those original variables need to be updated after fetchMore finishes (not necessarily to the same variables that fetchMore used, though they happen to be the same in this case):
diff --git a/src/App.js b/src/App.js
index 203c981..10f7214 100644
--- a/src/App.js
+++ b/src/App.js
@@ -11,8 +11,9 @@ const ALL_PEOPLE = gql`
`;
export default function App() {
+ const [skip, setSkip] = React.useState(0);
const { loading, data, fetchMore } = useQuery(ALL_PEOPLE, {
- variables: { skip: 0 }
+ variables: { skip }
});
console.log("FRONT DATA", data);
@@ -27,7 +28,10 @@ export default function App() {
<button
onClick={() => {
console.log("FETCH MORE");
- fetchMore({ variables: { skip: data?.people.length } });
+ const skip = data?.people.length ?? 0;
+ fetchMore({
+ variables: { skip }
+ }).then(() => setSkip(skip));
}}
>
Next page
However, in order for updating the variables to change what's rendered, you will also need to write a read function that takes args.skip into account, rather than always returning the complete list:
diff --git a/src/index.js b/src/index.js
index 237eecd..19bfc5b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -14,6 +14,14 @@ const client = new ApolloClient({
fields: {
people: {
keyArgs: false,
+
+ read(existing, { args }) {
+ if (existing) {
+ const offset = args?.skip ?? 0;
+ return existing.slice(offset);
+ }
+ },
+
merge: (existing, incoming, { args }) => {
const offset = args?.skip || 0;
console.log("MERGE", offset, existing, incoming);
See these docs for a discussion of the different kinds of read functions.
Hi, thanks for this very fast answer !
I updated the sandbox with your recommandations. However, the read function wasn't filling my needs so I changed it a bit (it was only rendering 2 people and stucked on page 2 because I'm using the array length to turn pages). According to the documentation, this behavior is default.
I also updated the logs output to better illustrate my issue. I don't understand why the log "FAKE SERVER CALL" is triggered twice when I call fetchMore function (only with network-only or cache-and-network).
// FIRST CLICK :
FETCH MORE
FAKE SERVER CALL {skip: 2}
FAKE SERVER CALL {skip: 0}
// SECOND CLICK :
FETCH MORE
FAKE SERVER CALL {skip: 4}
FAKE SERVER CALL {skip: 0}
Then, I added the const [skip, setSkip] = React.useState(0); as your recommandations and now I have :
// FIRST CLICK :
FETCH MORE
FAKE SERVER CALL {skip: 2}
FAKE SERVER CALL {skip: 0}
FAKE SERVER CALL {skip: 2}
// SECOND CLICK :
FETCH MORE
FAKE SERVER CALL {skip: 4}
FAKE SERVER CALL {skip: 2}
FAKE SERVER CALL {skip: 4}
Which is not really better =/
I have a similar issue #7314, with an additional complication related to using an object as the only input to my query. I tried the setState trick with no luck. If I do setState as suggested, I actually see THREE queries instead of two. First, the refetch with the correct offset, then a query with the original offset, then a third query with the correct offset again.
Edit: I'm not sure about @jgan42's use case, but in my use case I want the cache to show everything in a single list, not individual pages. From the code sandbox, it looks like the same thing. 3 fetches rather than 2 with the setState. I believe that suggestion was if you just want a single page, rather than a concatenated list of results.
Yes, for the "single page" results it is done with the read function adding the "slice offset" in it. Because I want an infinite scroll behavior like you, the default behavior of "read" is to return the entire "existing" which is good for me. (Even if I rewrote it with returning the entire existing array).
And like you, adding setState send 3 requests and it is not better, as described in my last post.
@jgan42 @CaptainStiggz You may want to switch back to fetchPolicy: "cache-first" after the initial request, since network-only will always go to the network, hence the extra network requests. To to that, use nextFetchPolicy:
const { loading, data, fetchMore } = useQuery(ALL_PEOPLE, {
variables: { skip: 0 },
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-first',
});
This seems to be what I need.
I updated the sandbox to work like this :
typePolicies: {
Query: {
fields: {
people: {
keyArgs: false,
merge: (existing = [], incoming, { args }) =>
args?.skip ? incoming : [...existing, ...incoming]
},
}
}
}
}
Moved the list into a subcomponent which can be rerendered.
The original wanted behavior : The user can "infinite scroll in the page". Then move from the list page, come back and start again with server data from the first page. Now it works.
I did see nextFetchPolicy at one point but it seemed odd to me. Using fetchPolicy: 'network-only' + nextFetchPolicy: 'cache-first' makes me think that the query will only work as "cache-first" only. Because if there is no cache, it surely will get the data from the server the first time. With the sandbox test, It now makes me think that fetchPolicy + nextFetchPolicy will always use the first policy on every new render.
I couldn't find any documentation about nextFetchPolicy (despite the fact that it exists as "any" value), nor about fetchPolicy but the list which the behaviors aren't clear to me : "cache-first" | "network-only" | "cache-only" | "no-cache" | "standby" | "cache-and-network"
if @CaptainStiggz can tell if it fills his needs, I will close this issue.
Most helpful comment
@jgan42 @CaptainStiggz You may want to switch back to
fetchPolicy: "cache-first"after the initial request, sincenetwork-onlywill always go to the network, hence the extra network requests. To to that, usenextFetchPolicy: