jaydenseric / graphql-react

A GraphQL client for React using modern context and hooks APIs that is lightweight (< 4 kB) but powerful; the first Relay and Apollo alternative with server side rendering.
https://npm.im/graphql-react
MIT License
718 stars 22 forks source link

Prevent unnecessary loading after SSR #4

Closed jaydenseric closed 5 years ago

jaydenseric commented 6 years ago

At the moment, Query components with the loadOnMount prop automatically fetch fresh data when mounted on the initial page load. This is unnecessary after SSR as the hydrated cache can be assumed to be fresh.

While harmless, and in some cases useful (if data becomes stale in the time it takes the page to load), it would be nice to work out how to prevent this to reduce the burden on servers and reduce client network traffic.

vladshcherbin commented 6 years ago

@jaydenseric hey, any news about this one?

jaydenseric commented 6 years ago

Not yet! It's a bug or a feature, depending how you look at it, so it's a low priority for me. I can't think of an easy way for the Query components to tell when it is the very first render on the client after SSR, so don't load.

jaydenseric commented 6 years ago

I suppose it would be nice to come up with a solution for when preloading on the client too. Say if you preload a route component and then mount it, you probably don't want all the queries to loadOnMount.

Maybe we introduce a new expires cache key, so that loadOnMount only does so if the cache has expired. It might make sense to then introduce a Query component prop (with a default, say 1s) for the cache expiry time.

That way if the page load takes ages, the client will still helpfully load fresh cache.

vladshcherbin commented 6 years ago

@jaydenseric yep, this feature would be really nice. Can I dm you a few other questions somewhere to not pollute this issue?

jaydenseric commented 6 years ago

We can Twitter DM: https://twitter.com/jaydenseric

vladshcherbin commented 6 years ago

@jaydenseric seems like I can write you only if you follow me.. 🤦‍♂️

jaydenseric commented 5 years ago

Having lived with it for a while now, I think I am happy with the current behavior.

quantizor commented 5 years ago

@jaydenseric Why? It's wasteful. Using a simple useEffect with empty dependency array is sufficient to know when the component has been mounted browser-side.

jaydenseric commented 5 years ago

@probablyup useEffect is already being used to load on mount:

https://github.com/jaydenseric/graphql-react/blob/v8.1.2/src/universal/useGraphQL.mjs#L163

The problem is, there is no way to tell if the component is mounting in the browser for the first time after SSR.

quantizor commented 5 years ago

Correct, which is why you need a second useEffect with an empty dependency array. With empty deps it only fires when mounting in the browser for the first time. https://reactjs.org/docs/hooks-effect.html (See the yellow notes section, paragraph 2)

jaydenseric commented 5 years ago

With empty deps it only fires when mounting in the browser for the first time.

It will fire whenever the component is mounted, not just the first time it is mounted. Components can be mounted and unmounted many times as a user interacts with the page long after the first SSR page load.

quantizor commented 5 years ago

Right, but what's the harm in firing it again if the developer fully removes and remounts the component? That seems reasonable, but firing the query again with a completely-same component tree and a valid cache doesn't make sense.

jaydenseric commented 5 years ago

You're just describing the original issue.

I've had an idea though.

As a standalone context and hook (could even be published separately), there could be a context called something like IsFirstMount that starts true, then in a hook via useEffect updates to false. I'm don't think it needs to be state, as when it changes we don't want to trigger the app to re-render. This context and hook needs to be used at the top app level, above any route components; otherwise loadOnMount won't work when navigating back and forth between routes.

I'm pretty sure child components mount before their parents do, so if they read from the IsFirstMount context they can tell if it's the first mount or not. Our useGraphQL hook could try to read the IsFirstMount context, and if it exists, and is true, and there is cache available for the query, decide to bail on loading on mount.

jaydenseric commented 5 years ago

In the end after building it, my idea didn't work because the effect callbacks can be called multiple times in the render storm that happens when the client hydrates. The first time it works, but then it thinks IsFirstMount is false for following render passes.

The solution was to add a new GraphQLProvider component that provides both the GraphQLContext and a FirstRenderDateContext, populated by a ref initialised with a new Date() at first render. Descendant useGraphQL hooks can check how much time has passed since the initial app render to decide if it is still within the arbitrary duration of time that can be considered the initial client hydration (500ms).

jaydenseric commented 5 years ago

Published in v8.2.0 🚀