adamsoffer/next-apollo

Question: is it possible to use with getStaticProps()?

Opened this issue · 16 comments

Dear Adam,

Thank you for this wonderful package. Please feel free to ignore this question if it is silly. I am struggling to follow what is going on...

We are using your package and would like to fetch data in getStaticProps() (in conjunction with the next-mdx-remote package.)

I found this thread and noticed you had commented on it.

Am I right in thinking that in order to fetch data in getStaticProps(), we cannot use your package, and will instead have to try to copy one of the solutions in that thread?

Thank you so much for any pointers and have a lovely Christmas,

Lydia

Hey Lydia - yeah that's correct. You'll have to copy one of the solutions in that thread. Take a look at this boilerplate. Might look into updating this package to abstract some of that boilerplate.

Hey Adam,

Thanks so much for replying and for the suggestion.

I'm looking forward to how everything will work with React's new server components. Maybe it won't make any difference though!

Have a nice new years!

@adamsoffer What are you looking for with the abstraction? Would it be sufficient to basically lift getStaticApolloProps from the boilerplate you linked? I'm thinking it could be taken pretty much entirely, except that it accepts an ApolloClient instance, e.g.

getStaticApolloProps({
  client: new ApolloClient({
    uri: 'https://48p1r2roz4.sse.codesandbox.io',
    cache: new InMemoryCache(),
  }),
})(MyComponent);

I wouldn't mind copying the code over and testing it if that's all you're looking for.

@chrbala - yeah pretty much! That would be awesome

Added a PR - let me know what you think!

I've discovered that globalApolloClient has an issue with getStaticProps: it uses the cache of the initial page, requiring a refetch on page transitions.

From what I can tell, there are a couple possible solutions:

  1. Remove this feature entirely. Clear the cache on all page routes. I actually think this is a reasonable option, since page transitions are a natural time to evict the cache. I understand that this may be a breaking change and is undesirable, however.
  2. Remove the automatic caching on a configurable basis, e.g. withApollo({ ssr: true, clearCacheOnPageEntry: true })(Page). Gives a lot of control, but the developer most likely will always want to clear the cache on entry to pages with static props.
  3. Clear the cache on page entry when getStaticApolloProps is used, by passing a clearCacheOnPageEntry prop to the WithApollo component. This is transparent to the developer.
  4. Append the data to the cache on page transition. This could be a rabbit hole on implementation, since there doesn't seem to be an obvious way to accomplish this. cache.modify seems like the best tool for the job, but I'm not sure how you'd actually use it to merge the cache.
  5. Some other combination of the above in a configurable way.

My inclination is to do option 3 for now, and it can remain the default option if and when additional configuration is added later on. Thoughts?

Hey @chrbala - options 3 sounds good and the PR looks good! I'll try and cut a release this weekend. Thanks so much for this.

I updated the branch in the PR with option 3. I didn't add it to my testing repo, but I did test it to make sure it works in my application.

Cool thanks. Question I just thought of. Consider this scenario using option 3:

  1. A user lands on a page that that does not use getStaticApolloProps. The page fetches data from apollo that populates some list.
  2. The user then navigates to a page that does use getStaticApolloProps.
  3. The user then navigates back to the first page.

In this scenario, will apollo have to refetch the list data since option 3 would have cleared the cache?

Yes, they'll have to refetch the data in that case. We could replace if (clearCache) globalApolloClient = null; with if (clearCache) return createApolloClient(apolloClient, initialState, ctx); to do this. We'd have 2 ApolloClient instances existing simultaneously, even though only one is in use per page, so we'd have to either clone them (is this possible?) or accept a generator function () => client instead of an instance client.

If we go that second route, we'd have to either make a breaking change to the library API, or handle the ambiguous case where someone uses withApollo(client) together with getStaticApolloProps. Do we throw to ensure consistent experiences and to avoid having to handle the weird case? We could handle that in way I initially proposed, but it seems like we'd be optimizing for a case that shouldn't really be supported.

So, what I'm proposing with all of that in mind is that withApollo(client) can also be called like withApollo(() => client) and it must be called the second way if getStaticApolloProps is used on that page, which is inferred from clearCacheOnPageEntry existing on WithApolloOptions.

(I'll also say that I don't exactly have a horse in this race for now, aside from shipping getStaticApolloProps. I'm only doing SSG and no SSR on my own app. So I'm also fine with the current behavior I proposed initially, and would be happy leaving the more aggressive cache clearing behavior as an open issue.)

Gotcha. Thanks for the explaination!

I think it's actually fairly common for apps to use getStaticProps on some pages but not others so we'll want to account for this. For example, we'd want to use getStaticProps for a site's blog pages, but not for a site's user account pages. I do this on one of my own applications.

I've discovered that globalApolloClient has an issue with getStaticProps: it uses the cache of the initial page, requiring a refetch on page transitions.

I think I'm slightly confused about what the issue is. Would you mind explaining how and why the above would cause a refetch when it's not supposed to?

Yeah, basically the static props are the initial state on the first page load. You only get one first page load until you do a full HTML page refresh, so that's the only time you pull in an initial state. If you navigate from a page that already has an apollo client, you have to (1) merge the initial state from the page props into the current Apollo cache, (2) download it from the server (which Apollo merges into the current cache behind the scenes), or (3) reset the cache and set the page's apollo client with the new page's initial state.

Each of those options has their own downsides.

  1. Probably difficult to accomplish on the library side, and may be error-prone.
  2. Slow and inefficient, and will break some use cases, like if there is no public endpoint.
  3. Loses existing cache state for the SSG pages, at least. Optionally, we can have a shared cache for SSR pages that gets saved, even with transitions to and from SSG pages. This is what I proposed above.

hm cache seems to work fine on page transitions on next-apollo-ts. Is this necessary?

Yeah, it is. I added a repro that intentionally breaks fetches to illustrate the issue, so that all data must come through the SSG props. Data doesn't load correctly when the page transitions between / and /second. Only the data on the first page ends up in the cache. You can see that the bogus endpoint gets called in the network tab as well, on the page that is loaded from the links.

Gotcha. Sorry for dropping off.

So I'm using next-apollo-ts on one of my projects and for some reason, the cache works as expected when navigating between a page rendered using SSG and another rendered client-side.

Notice when you click the first row on the table of this statically generated page you'll see a spinner. When you go back to the "orchestrators" page and then click the first row again and it gets served from apollo's cache and there's no spinner. I can't explain why but it seems to be working as it should. 🤷

There are enough requests going on with your network history there that I can't really trace it down easily, but I think I know what's going on there. Actually, the experience you're describing indicates that the library isn't actually using the SSG data on route transitions.

When you load the statically generated page, the data doesn't exist in the cache, so Apollo fills it in with a network request. That's why you see the spinner. If that page used the SSG data, there wouldn't be a spinner because the data from the SSG function populates synchronously. The second time you load the page, the server data has populated the cache, so it loads synchronously in your case as well. After the cache has been populated, both versions are indistinguishable in your case: it's just the first load that changes.

In your case, the app is functional, but takes an extra network request the first time those pages load. If an app only uses SSG and has no live endpoint, they can't use links from 'next/link' for fast routing, and would have to use <a /> tags to flush the document and start again with the new SSG data.