apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
406 stars 30 forks source link

using fetchPolicy/nextFetchPolicy across server/client rendering in client components #324

Open awinograd opened 3 weeks ago

awinograd commented 3 weeks ago

Our goal with apollo client is to configure the following behavior:

  1. Always fetch data from network when a component mounts/remounts (to avoid stale data)
  2. Allow using data from the cache while the network fetch occurs
  3. Use data in cache for re-renders

We use the following watchQuery options which implements the above in a fully client-side rendered environment.

        watchQuery: {
          fetchPolicy: 'cache-and-network',
          // We want to rely on the cache when a component re-renders (not remounts) rather
          // than send a new network request on every render after a mutation that touched the cached object
          // https://github.com/apollographql/apollo-client/issues/6760#issuecomment-668188727
          nextFetchPolicy: 'cache-first',
          errorPolicy: 'all',
        },

However, now we're starting to incorporate SSR rendering of client components using useSuspenseQuery.

What I think should happen is that the SSR pass should use fetchPolicy: 'cache-and-network' and then once the client hydrates the first (and subsequent) render(s) would use nextFetchPolicy until the component is unmounted and remounted at which point we'd start back at fetchPolicy.

We observe in practice is a duplicate fetch for each query since one fetch occurs during SSR and the other during CSR. It looks like fetchPolicy/nextFetchPolicy application is not preserved across the server/client boundary. Is this intended behavior? Am I thinking about the implementation of fetchPolicy/nextFetchPolicy correctly? For full transparency I find the docs on fetchPolicy/nextFetchPolicy a bit confusing. It's not totally clear what constitutes an "execution" 🙈

As always, thanks for any help / thoughts you have!

phryneas commented 3 weeks ago

Hmm, this is a complicated mental model - please stay with me :)

When the fetch happens in SSR, two things happen in the browser:

  1. a "network request" is simulated and resolved with the value that same request on the server resolves
  2. the result of that is written to the cache

The reason 1. happens is so that other calls to useSuspenseQuery or useQuery can "latch onto" the network request running on the server.

Now, in the browser, once that component renders, 1. is likely to already have resolved because it has been suspended that time on the server. So, no more "ongoing network request" and the value is already in the cache.

Now, we get to your problem: components in the browser have no knowledge that they rendered on the server before. They just get mounted and behave as such. The cache will already be filled, but you apply 'cache-and-network', so they make a request anyways.

The best you could do here really is to use 'cache-first' - but of course, during the runtime of your application, that will not be satisfying.

I don't really have a good suggestion here, but maybe as a workaround you could have some kind of global recentlyLoaded variable that changes from true to false after 30 seconds, and only after that, your default fetchPolicy changes from cache-first to 'cache-and-network'?

awinograd commented 3 weeks ago

@phryneas How do you recommend changing the default fetchPolicy? It doesn't appear that fetchPolicy supports a callback fn like nextFetchPolicy does.

Or are you recommending something like:

  const client = useApolloClient()

  useEffect(() => {
      setTimeout(() => {
          client.defaultOptions.watchQuery.fetchPolicy = 'cache-and-network';
      }, 30000)
  }, [])
phryneas commented 3 weeks ago

Yes, I believe that should work.