apollographql / react-apollo

:recycle: React integration for Apollo Client
https://www.apollographql.com/docs/react/
MIT License
6.85k stars 790 forks source link

Infinite render when useQuery defines onCompleted with 'cache-and-network' fetchPolicy #4008

Open johnnyoshika opened 4 years ago

johnnyoshika commented 4 years ago

Intended outcome:

When using useQuery with onCompleted and fetchPolicy set to cache-and-network, I expected the network request to be triggered only once.

Actual outcome:

Resulted in an infinite render and infinite GraphQL requests.

Sample code:

  const { data } = useQuery(SOME_QUERY, {
    onCompleted: () => {},
    fetchPolicy: "cache-and-network"
  });

How to reproduce the issue:

Bug can be reproduced here: https://codesandbox.io/s/continents-h0822

Specifically in this file: https://codesandbox.io/s/continents-h0822?file=/src/Continents.js

The console will show this and execute 4 GraphQL requests before execution will be forced to stop:

render 1
render 2
render 3
render 4
render 5
render 6

Codesandbox's page can be run here: https://h0822.csb.app/

Version

  System:
    OS: Windows 10 10.0.18363
  Binaries:
    Node: 12.16.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.4 - ~\scoop\apps\yarn\current\Yarn\bin\yarn.CMD
    npm: 6.13.4 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: 0.00
  npmPackages:
    @apollo/client: ^3.0.0-rc.2 => 3.0.0-rc.2

Also reproduced in codesandbox: https://codesandbox.io/s/continents-h0822

johnnyoshika commented 4 years ago

This could be somewhat related: https://github.com/apollographql/react-apollo/issues/3333

thomassuckow commented 4 years ago

@apollo/client 3.0.0-rc.0 Changed the behaviour of onCompleted so it must be memoized in the component. We are seeing crazy infinite behavior and are trying to figure out a way in @apollo/react-hoc to deal with that.

The answer is probably just to port all the files that do it. Otherwise crazy fixes would be needed.

https://github.com/apollographql/apollo-client/issues/6301

In the meantime we went back to beta 45

johnnyoshika commented 4 years ago

@thomassuckow Oh, good to know that I can go back to beta 45 to get around this problem for now.

johnnyoshika commented 4 years ago

I quickly switched this sample app to use beta 45 and everything worked as expected: https://codesandbox.io/s/continents-h0822

So it is indeed a problem that was introduced after that release.

thomassuckow commented 4 years ago

I believe they are intending to keep it this way, which I can understand as it probably fixes a lot of edge cases. But it doesn't interact so well with react-hoc

johnnyoshika commented 4 years ago

@thomassuckow They're intending to keep the infinite loop??? Is that a strange decision?

thomassuckow commented 4 years ago

They are intending to keep the requirement that onComplete, et. al needs to be memoized. Since Apollo Client 3 is react hook based, that requirement makes sense (Rules of Hooks). The problem is how the method was typically provided to the HOC does not lend itself to memoization.

johnnyoshika commented 4 years ago

@thomassuckow Interesting. Is there documentation whereonCompleted and memoization is discussed? Can you point me to some sample code where a callback like that is memoized? Thanks in advance.

thomassuckow commented 4 years ago

I cannot for the life of me find what I swear was a dev or a commit stating they intentionally changed the behavior.

Regardless, When using useQuery the options object has onComplete. Anywhere that is used, the callback would need to be wrapped in a useCallback.

const f = useQuery(FOO, { onComplete: () => { doSomething(); doSomethingElse() } });

becomes

const callback = useCallback(() => { doSomething(); doSomethingElse() }, []);
const f = useQuery(FOO, { onComplete: callback });
johnnyoshika commented 4 years ago

@thomassuckow that works perfectly, thank you! So if I understand correctly, useQuery re-requests the GraphQL request if any of the paramaters are different (using shallow comparison, I assume).

thomassuckow commented 4 years ago

Yep. Which when the cache strategy is set to network-only causes an infinite loop if one of them isn't memoized.

millievn commented 4 years ago

I have two functions but same lazyloadquery in one page, one for loading by pagination and another for loading all data. Like this:

function useSomeBooksApi(){
  const [fetch,{loading,data}] = useBooksLazyQuery({fetchPolicy: 'cache-and-network'})
  const load = (groupId,page,size)=>{
     fetch({variables:{groupId,page,size}})
  }

 return {
  load,data,loading
 }
}

function useAllBooksApi(){
  const [fetch,{loading,data}] = useBooksLazyQuery({
     fetchPolicy: 'network-only', // or no-cache
     onCompleted(data){
       downloadCsv(data.books)  
    }
  })

 return {
  fetch,data,loading
 }
}

When I use those in one page, useBooksLazyQuery calls infinitely. But different lazyloadquery with same usage works fine.

Info: