Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.Sign up
Partial Hydration #14717
This adds a mechanism for partially hydrating a server rendered result while other parts of the page are still loading the code or data. This means that you can start interacting with parts of the screen while others are still hydrating.
In this model you always have to hydrate the root content first because it is what provides props to the children, which can be of arbitrary complexity. The model assumes that the root of the app is designed to be relatively shallow and then each abstraction gets progressively more complex the deeper it gets. To become interactive faster, components in the tree can themselves use progressive enhancement to add more complexity after initial hydration.
This mechanism works on top of the
The reason for the breadth first hydration is because we need to fully complete the parent before we can make any updates to children. Once we have committed a parent, we can now prioritize any of the Suspense "holes" left dehydrated inside it - independently.
The reason this works with Suspense boundaries is that the parents already have to be resilient to the children not being fully resolved, but also because we can at any point trigger the fallback state to return to a consistent state.
If the Suspense component rerenders with new props or new context as input, before we've fully hydrated, we have two problems:
Therefore, the semantics here is that if that happens, then we'll delete the existing content and rerender it from scratch. If that suspends, we'll show the fallback.
This is an undesirable experience but reasonable compromise. To avoid this the product code must:
This solution is susceptible to tearing issues, common in Flux stores, just like Concurrent Mode in general. E.g. if the store is mutated before the next level is hydrated, then we'll try to hydrate it with the wrong initial state. Therefore, stores need to be able to save a snapshot of their initial state for the duration of the hydration.
In the current version of this PR, hydration of suspense boundaries always gets scheduled as concurrent. I'm not sure if a non-concurrent mode of this even makes sense.
Hoisting state up to the root can be problematic because as these update they will pass their value down and rerender components that may not have fully hydrated yet which will put them in their fallback state. The key is to make any such state have a long expiration time and long suspense time so that if it happens, we have time to hydrate it beforehand. High-pri state should be local to components and not rerender at the top. Updates to top level state placed in Context will force the fallback state of the whole tree since it can affect everything and needs to be managed carefully.
For these reasons, it is important to carefully design the shell of the app so that different parts can operate independently for at least some period of time.
Left to do in this PR:
Follow up 1:
Follow up 2:
Details of bundled changes.
The bad case in this solution happens when a props/context changes which then causes that update to suspend. This triggers the fallback state. In the fully client-side solution, we keep the state while we load the missing data. However, in this case we'll delete the server rendered nodes and lose their state.
With one of the followups this won't happen as long as we can hydrate before the timeout happens because we can hydrate right before committing the suspended state.
It might seems like we should be able to hide the server rendered content and then hydrate it and show it. That's slightly different because if we commit the fallback state, we have now lost whatever was "current" right before that. We've lost the props and the context of all parent components - which we need to hydrate the child in its original state.
In theory we could do something advanced in this case like snapshotting the props and values of all contexts. However, we'd have to keep this indefinitely and rely on that these snapshots are the only sources of data and that they're fully immutable. This adds a new constraint, that we're able to render states older than current.
Even if we could, we don't want to replay clicks when a fallback was rendered between. Since it's not seamless anyway. The only thing we'd gain is the ability to preserve state in uncontrolled forms.
This doesn't seem worth it.