Replies: 2 comments 4 replies
-
Bonus: "Incremental" external store hydration
A problem related to SSR hydration, but with a whole different set of challenges is how external stores can support metaframeworks that provide capability to abstract data fetching to the server, even on client side navigations. Next.js In this case, we don't have anything equal to
The first one is common today in React 17. The most popular Redux wrapper for Next.js hydrates in a constructor/shouldComponentUpdate and React Query does it in render (I wrote the RQ one btw so I don't mean to shame, just point to current tradeoffs!). While a common approach, this is an unsafe side effect in the render phase since at this point we might have listeners for that data. This might well lead to edge cases already and these might get more common in 18(?). The second option requires us to stay in a loading state unnecessarily, and possibly switch to a separate loading state from what the metaframeworks themselves provide, which leads to a less than optimal UX even if very quick. It can also have the drawback of loosing state in components that would otherwise stay on screen. (There might be a third option, keep the store state on a context instead, since a modified context can be passed down safely inside of render. That has other problems though and besides, then the store wouldn't really be "external" anymore. A variant of this might be to keep only the "new" data on a context until an effect can hydrate it properly?) If anyone has any good ideas or advice on how to tackle this I'd be really grateful! (Feels dirty and wrong, but is this that elusive case where a For now the working hypothesis is to switch React Query from hydrating in render to in |
Beta Was this translation helpful? Give feedback.
-
To your original main question:
Yes. I don't see a problem with it, other than maybe performance because that store might be recreated when something suspends at the root. Probably not a big deal. |
Beta Was this translation helpful? Give feedback.
-
This subject has been touched upon in other places of this working group, in the discussion about
useSyncExternalStore
(and specifically thegetServerSnapshot
argument), as well as in the Library Update Guide: <script> under the header "Hydration Data".Since this is an important subject for most/all external stores that want to support SSR I wanted to pull together a quick summary to verify that I've understood things correctly.
Since the initial React 18 release wont have official support for Suspense data fetching, I'll only talk about the case where you have all data available up front on the client, not streaming data hydration (the above Library guide has some great notes on that though).
Background
State affects markup, and because markup has to be equal when rendering on the server and hydrating that markup on the client, we have to start with identical state in both cases. The normal way to do this is to embed the state used on the server in the markup, and prime the external store with that.
The thing that changes in React 18 is that markup hydration can now be "async" (if
<Suspense>
is used) and because state might update in between chunks of markup hydration, we need some way to guarantee the hydration process always reads the initial external store state to avoid hydration mismatches.Solution
The new
useSyncExternalStore
hook has a third argument, calledgetServerSnapshot
. This will always be used to read data from the store when React is hydrating markup, so if we pass in a callback that returns a frozen/immutable version of the initial store state there, we will avoid markup hydration mismatches, even if state changes during hydration. If state has changed, React will immediately rerender with the new state, so while UI might get temporarily inconsistent during hydration, it will always end up in a consistent state(?).When to add data to/hydrate the actual store
As long as we don't use Suspense data fetching, all data still needs to be fetched up front on the server, before rendering. This means it can always be available before hydrating on the client, so the recommended way to hydrate the external store is to do that when creating the store.
A caveat is that in some metaframeworks, there is no way for developers to ergonomically run code before hydration starts, in which case the external store needs to be created inside of render. This should be fine, at least if store creation has no observable side effects, since nothing is subscribing to the store at this point?
In essence, would something like this be fine for store creation?
Behind the scenes, this external store should grab a frozen copy of the
initialState
and pass that down to anywhere it uses theuSES
to read data from the store, so it can be passed in (in callback form) to the third argumentgetServerSnapshot
.Is this correct, am I missing something? Is there any extra nuance/edge cases to be aware of?
Beta Was this translation helpful? Give feedback.
All reactions