Replies: 3 comments 3 replies
-
I can't give feedback to most of your post, but I would like to give some for one topic: The proposed API for // inside getServerSideProps / getStaticProps
const dogIdToFetch = "dog-id-1"
const dogFetched = await fetchDog(dogIdToFetch)
const queryClient = new QueryClient();
// "dog" query
await queryClient.setQueryData(
["dog", dogIdToFetch],
dogFetched
);
// the data for the dog could be used in some more detailed query, like "poodle"
if (dogFetched) {
// "poodle" query
await queryClient.setQueryData(
["poodle", dogIdToFetch],
dogFetched
);
} This happens before dehydration with the current API, so a user is free to prefill queries as much as he wants. |
Beta Was this translation helpful? Give feedback.
-
Great analysis, thank you 🙇 .
I'd like to focus on this aspect specifically, as we have the and then create a subscription that syncs the store to the external storage. This plugin uses Mainly because we don't block / defer rendering of the app while we are still restoring from the external store. |
Beta Was this translation helpful? Give feedback.
-
I think we need to do some breaking changes to how hydration works in RQ and here I’ll try to outline why and the proposed changes. It is very technical and primarily targeted at maintainers and contributors to React Query, but I wanted to put the discussion in the open and outside feedback is very welcome! 😃
Whatever we land on, it will be documented clearly in a migration guide so no need to worry about this yet if you are a user of React Query.
The first reason we want to change hydration is because of changes in React 18. To support concurrent features and progressive/selective markup hydration, RQ is moving to use
useSyncExternalStore
, which requires us to make a bunch of behind the scene changes around hydration which in the end will affect the external API. I’ll describe this under initial hydration.The second reason is that we are currently hydrating inside of render, and not in a
useEffect
. This was done for a bunch of reasons to better support metaframeworks like Next.js, but I ultimately think this was a mistake (mine) and is likely to have more noticeable negative consequences with concurrent features. I’ll describe this under progressive hydration.Goal
To be clear, I don’t think we should be aiming to support streaming rendering initially. Data fetching with Suspense on the server wont be officially supported in the initial React 18 release and while technically possible, it’s a whole different beast to tackle. Instead the goal is to:
getServerSideProps
)Initial hydration
Initial hydration is a matter of answering the question:
This has been relatively straightforward previously because markup hydration always happened in one synchronous operation, there was no chance for the data to change from when rendering started to when it finished.
In React 18, markup hydration can happen in asynchronous chunks (each Suspense boundary by itself). Between one part of the application hydrating and another, the cache might have changed, which could lead to markup mismatches between what was rendered on the server, and hydrated on the client.
Thankfully,
useSyncExternalStore
makes this relatively easy to deal with. It’s third argument takes agetServerSnapshot
callback and the result of that callback is used as the state for hydration. So to support progressive/selective hydration in React 18 we need to:uSES
hook behind the scenes and pass it in thereI think the most straightforward API for this is that if you use SSR, we require you to pass in what is called the
dehydratedState
in the docs directly when you create the QueryClient. Perhaps something like:That way we can prime the cache initially so data is already there, and we can take care of capturing a snapshot of that and provide it to
uSES
behind the scenes.This is how the hydration API was designed initially, but this was changed to provide it as a separate entrypoint instead. Now hydration is part of core so I think this approach makes a lot of sense.
If all you are doing is hydrating initial data from the server, and don't use anything like
getServerSideProps
, you could go ahead and remove all calls touseHydrate
and just pass it in initially and everything would work.An alternative could be that the user provided it to a context provider instead, but I think this makes for a less straightforward API.
Progressive hydration
This one is trickier. It is a matter of answering the question:
There are a few cases we want to support (might be more):
getServerSideProps
in Next.js, or loaders in Remix)For "other external sources", hydrating in a
useEffect
makes sense and has no negative tradeoffs. When the effect runs, data is added to the cache, which triggers rerenders in any components listening.For data from the server on page navigation however, there are downsides to doing it in
useEffect
. Because that happens after render, data wont be available in that first render, forcing us to show a loading state before the hydration happens and we can render the real thing, even though we already have the data we need. This can also lead to unnecessarily tearing down existing components and rendering them again, losing state etc (see for example this related issue in next-redux-wrapper). This is why it is currently done in render.The optimal place to hydrate extra data from the server into the cache in metaframeworks would be in some lifecycle before the framework kicks off rendering the next page. Something like
receiveServerProps
or the like, but no frameworks currently supports this. See related discussion in Next.js.Even though doing it in a
useEffect
has negative downsides, I think with concurrent features that's a better option than keeping it inside of render. This is more "correct" and definitely more future proof. Even without concurrent features, I think we might have edge cases with this today.Possible alternative
The only way to safely pass something to deep children inside of render itself is putting that thing on a React context via a Provider. A possible way to get around both hydrating in render and the negative downsides of
useEffect
could be to in addition to hydrating inuseEffect
also pass down thedehydratedState
on a context (the provider for this could also be the one to calluseEffect
behind the scenes). If somedehydratedState
lives on that context, on cache read we merge that temporarily (without writing it to the cache) the same way we do when hydrating, and when theuseEffect
has happened, we remove it from the context since now it's already in the cache.I think this would work with concurrent features (nothing is written to the cache until
useEffect
) and avoid the downsides of doing it only inuseEffect
(like unnecessary loading states, fresh data is available directly on the first render).This puts a lot of implementation burden and complexity on RQ though, for something that is a problem for many external stores (like Redux just as an example, next-redux-wrapper faces the same problem as mentioned in the issue linked earlier).
Because of all the tradeoffs mentioned here I'm less certain about the best solution for progressive hydration, but I think we should move
useHydrate
touseEffect
and take it from there.Since the low level imperative API
hydrate()
exists, users that want to keep the old behaviour of hydrating in render could create their ownuseHydrate
-hook with the old behaviour (which we could document in a migration guide).A note on Suspense
I think RQ in it's current form could be compatible with Suspense SSR as long as you don't do streaming, but more experimentation is needed.
If you use Suspense for data fetching and reading, I think it makes sense to also hydrate with Suspense. That is, you throw a synchronous thenable (kind of an "immediate Promise") which adds the data to the cache and resolves any queries currently waiting on that data. If this happens during the current render, it will rerender immediately and never show any fallback (behaviour documented in this test in React). I thought about possibly always using Suspense for hydration, but I think that will lead to problems (tearing and worse) when the data fetching itself is not Suspense based. Think about the case where a render is concurrent, we add data to our cache inside of Suspense and then that render aborts. The data is now in the cache and all queries currently listening to that data will have updated early.
A note on streaming and Server Components
I'll just note that this has some overlap with progressive hydration described above, but with a bunch of added complexity.
This is a good discussion on this. Also, this post in the React Working Group has some interesting notes under the header "Hydration Data".
I'm looking forward to see what the React team comes up with here, but there is nothing stopping us from experimenting with this ourselves after the initial release to support React 18.
Summary of proposed API changes
initialCache
(or similar) as an option when creating anew QueryClient()
. This is required to pass in when using SSR, you can not use onlyuseHydrate()
as before.useHydrate()
from happening in render to happen in an effect. This doesn't change the external API, but is a breaking behavioural change.Beta Was this translation helpful? Give feedback.
All reactions