-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[defer-hydration] Async hydration #48
Comments
I don't think the use case you're taking about or the example your posted is really hydration in the sense that I know it. You essentially have a component that's already "hydrated" or just doesn't need hydration. One case where defer-hydration would be critical is if instead of fetching data based on a Regardless, you have a component that async fetches data, and therefore all of the APIs based on that data need to be designed with that in mind. Whether |
I can see that
Sure, if you believe that is a more compelling use case then I agree async hydration could be just as relevant there. I created a new Stackblitz which demonstrates this exact use case. I don't personally see
I will point out that not all components will support client-side rendering. Server-only components which are progressively enhanced on the client that don't necessitate a re-render step are perfectly viable today. An async form of That said, I do agree client-side rendering has a very similar initialization problem, and maybe that's a signal that this is something which should be tackled independently of |
I happened to be looking through the First, it waits for its conditions to trigger the hydration of its children. Essentially if you use One could argue that Second, it has an extension which dynamic imports dependencies and initializes them. You can write: <is-land import="./some-component.js">
<some-component defer-hydration>
<!-- ... -->
</some-component>
</is-land> It will automatically import With both of these in mind, the question becomes: Would a consumer of Prior to The one counter argument I can think of is that it might make more sense to directly observe the element you actually want ( In terms of motivation, I literally discovered that I mainly just wanted to mention |
Since the
defer-hydration
proposal seems to be moving forward I'd like to start one point of discussion: Is hydration a fundamentally synchronous or asynchronous process? I don't know of a strict, formal definition of "hydration" which can answer this question, but the proposal currently uses the following definition:This issue mainly boils down to answering the question: Is the process of re-associating a template with server-rendered DOM always synchronous?
Use case
I think there is a case to be made for async hydration. Consider a component which needs to load its own data asynchronously before it is fully functional. For example, consider a component which shows a user with a large number of friends. We might not want to list out every friend in the initial HTML, because some users can have thousands of friends. Instead, we might choose to lazy load this list of friends and render it when it becomes available (possibly with streaming or other cool tricks). Full example on Stacklibtz.
Ok, so we defined our own
hydrate
method and made itasync
. Web components are free to define their implementations and this is fine on its own. It's basically just a "slow" hydration. The problem comes when we try to expose this async data such as agetFriends
method.This might seem like a simple addition, but it completely changes the lifecycle of this component as we now have a timing bug. This code assumes
hydrate()
has fully completed its async work beforegetFriends
is called. However, the promise which awaits this data (the return value ofhydrate()
) is not accessible in a generic manner. For example, if we tried to hydrate and use this component according to thedefer-hydration
specification, it would look like:We're forced into some uncomfortable design decisions. I can see a few potential solutions to this component which don't involve modifying the
defer-hydration
proposal:Wait via a
my-user
-specific APIOne approach is to have
my-user
define its own API users should use to know when it is done hydrating asynchronously:Downsides:
removeAttribute('defer-hydration')
) may be very far away from the code which callsgetFriends()
and may not know it is looking at amy-user
component or thatdoneLoadingUser
exists.Implicitly hydrate in
getFriends
Another approach is for
getFriends
to automatically hydrate before returning:Downsides:
async
, even when it doesn't actually do any async work beyond hydration.getFriends
will hydrate the component or apply any associated side effects (trigger network requests, add event listeners, modify the component UI, etc.).this.removeAttribute('defer-hydration')
or it could misrepresent its current hydration status.getFriends
or when the returnedPromise
resolves? Is the component "hydrated" when it starts hydrating or when it's done hydrating?defer-hydration
is manually removed by a parent component. In that scenario,defer-hydration
is removed at the start of hydration, but callinggetFriends
would remove it at the end of hydration.Both of these approaches effectively treat hydration as the synchronous process of reading the DOM state (the
user-id
attribute in this case) and providing a separate "initialization" process for consumers to know when the component is initialized and ready. Since initialization is a different, out-of-scope process from hydration, component consumers cannot make any generic inferences about how initialization will work.Async data takes a lot of forms. One can imagine a framework which identifies large component trees and pushes some hydration data out of the initial page response to reduce the initial download time. Then on hydration, components may fetch the data they need to hydrate in order to make themselves interactive. I'm not aware of any framework which quite does this (I don't think Qwik or Wiz work this way), but it is an interesting avenue which could be explored in the future and would be incompatible with
defer-hydration
as currently specified.Straw-proposal
Just to put out one potential proposal which could address this use case in the community protocol, we could define a
whenHydrated
property on async hydration components (mirroringcustomElements.whenDefined
). This property would be assigned to aPromise
which, when resolved, indicates that the component is hydrated. In practice this would look like:Then, when hydrating a component we can generically check if async work needs to be done.
This proposal supports async hydration in a generic and interoperable manner.
Discussion
To be clear, I'm not necessarily trying to argue
defer-hydration
absolutely should support async hydration. I'm not fully convinced this is a good idea either, but I do think it's something worth discussing at minimum.Hydration vs. Initialization
As I've hinted a bit earlier, I suspect the concept of "async hydration" is somewhat intermingling two independent concerns: hydration and initialization. An alternative definition of "hydration" can more narrowly specify the concept along the lines of "Reading initial component state from prerendered DOM". Based on this definition, the
my-user
component described above does more than just hydrate itself.Number(this.getAttribute('user-id')!)
is the only real "hydration" the component performs. Everything else is completely unrelated initialization work which applies both in CSR and SSR use cases. Fetching data from the user ID and updating the DOM can be considered "initialization" rather than "hydration".If we accept this more narrow definition of "hydration" and call initialization an independent problem which is out of scope of
defer-hydration
, then there's no bug here and the proposal doesn't need to change at all. Understanding when an object is initialized has been a problem for as long as we've had objects after all.OTOH, we could define "hydration" along the lines of "Making the component interactive to the user", I think it is entirely fair to expect some components will require asynchronous work before they can support interactivity. Here's another Stackblitz of a somewhat contrived use case which requires a network request of initialization data before buttons can be enabled. Calling such a component "hydrated" synchronously after
defer-hydration
is removed would be misleading because the component is still in no way interactive and has not presented any visual or behavioral change to the user.If we accept the separation of concerns between hydration and initialization, then
defer-hydration
becomes a much less powerful proposal. If "hydrated" does not imply "initialized" then it is hard to generically do anything with a component.I think my initial interpretation of
defer-hydration
was that it could serve as a signal that a component was initialized and fully functional. It's entirely possible that interpretation was incorrect, but I do still think that's usually true, and it provides a lot of power when working with components in a generic fashion. I suspect makingdefer-hydration
support async use cases could further enable hydration to serve as an initialization signal if we think that is the right approach to explore.Again, I'm not totally sold on the idea of "async hydration" either. I just think its neat.
The text was updated successfully, but these errors were encountered: