-
Notifications
You must be signed in to change notification settings - Fork 13
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] Initial draft #15
Conversation
|
||
In server-side rendered (SSR) applications, the process of a component running code to re-associate its template with the server-rendered DOM is called "hydration". The defer-hydration protocol is design to allow controler over hydration to solve two related problems: | ||
|
||
1. Interoperable incremental hydration across web components. Componentents should not automatically hydrate upon being defined, but wait for a signal from their parent or other coordinator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why this is (why they shouldn't automatically hydrate)? Is there an example where doing so would create unwanted results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see this fitting into a "when work gets done" topic of conversation by allowing islands to be linked generally by component name, while still being able to disambiguate on which of those islands are needed at any one time. Much like Astrobuild leverages the :visible
modifier to control when the code for a component is loaded, this could allow other instances of the component on the same page the ability upgrade with the initially visible instance, and then wait to hydrate until they, too, are in view.
I think this sort of laziness deserves to be revisited more thoroughly a the browser spec level. This seems roughly in line with the a proposal around lazy definitions: WICG/webcomponents#782 and could arguably fit into a larger conversation around the browser allowing for lazy upgrades, which would be somewhat similar to what this protocol looks to support.
Regardless, if you're SSR'ing the entirety of an element that may not come into view, the ability to control when it reconciles that delivered SSR render with the client-side render sounds like a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for just now getting back to this, but why would you want to delay rendering a component? The reason for those modifiers are to:
- Deprioritize loading of the code so that more important assets can be loaded over network.
- Deprioritize executing the component (and its dependencies) JS so that more important assets can use the CPU instead.
The reason is not to delay rendering. Rendering should be relatively cheap, especially if the component was prerendered, there's no work to do but set up the event listeners.
It is, of course, possible that some components are going to be more expensive than others, but that's reason for those particular components to have their own API to delay rendering, not to make it a standard that is used arbitrarily.
The goal is to make the page as interactive as possible. Delaying rendering without good reason should be avoided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, the main reason is to enable top-down initialization, even for spontaneous first renders. We need our components to initialize in the same order whether CSR'ed or SSR'ed, independent of definition order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a different use case (being discussed below). The island orchestration use case is another, it's why Eleventy/webc has adopted this for example.
In server-side rendered (SSR) applications, the process of a component running code to re-associate its template with the server-rendered DOM is called "hydration". The defer-hydration protocol is design to allow controler over hydration to solve two related problems: | ||
|
||
1. Interoperable incremental hydration across web components. Componentents should not automatically hydrate upon being defined, but wait for a signal from their parent or other coordinator. | ||
2. Hydration ordering independent of definition order. Because components usually depend on data from their parent, and the parent won't usually set data on a child until it's hydrated, we need hydration to occur in a top-down order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is maybe a partial answer for (1)? Should these be separate points, if so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about it, I don't think I agree with this point (2), cc @Westbrook. I would expect that in most cases an element's initial data has been serialized as attributes and doesn't need the parent to pass that information. The exception would be data that isn't serialized, like complex objects, but I don't think that's really the norm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The https://github.com/w3c/webcomponents-cg has a breakout session to discuss Community Protocols, that could certainly include this, at the beginning of next month. Would you be interested in joining the call? Would be great to get your thoughts on this, and many other of the topics we're discussing right now "in person"!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I could probably attend that.
Just for clarity, I'm not against this proposal, just trying to understand the scenarios in which it is needed. The text could help by clarifying that a bit more.
My thinking is that a typical usage would not need this because:
- Static imports take care of making sure children are hydrated first.
- Even if hydration happens out-of-order state should be serialized in attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@justinfagnani this looks like a great start.
What are you thoughts on this possibly being paired with a conversation at the spec level for revisiting lazy definitions, lazy upgrading, etc.?
Also, @liamdebeasi I wonder how you might see something like this aligning with the lazy loading strategy that Stencil uses. Feel free to ring in any of your teammates that would be better fit to the conversation, if you'd like.
|
||
In server-side rendered (SSR) applications, the process of a component running code to re-associate its template with the server-rendered DOM is called "hydration". The defer-hydration protocol is design to allow controler over hydration to solve two related problems: | ||
|
||
1. Interoperable incremental hydration across web components. Componentents should not automatically hydrate upon being defined, but wait for a signal from their parent or other coordinator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see this fitting into a "when work gets done" topic of conversation by allowing islands to be linked generally by component name, while still being able to disambiguate on which of those islands are needed at any one time. Much like Astrobuild leverages the :visible
modifier to control when the code for a component is loaded, this could allow other instances of the component on the same page the ability upgrade with the initially visible instance, and then wait to hydrate until they, too, are in view.
I think this sort of laziness deserves to be revisited more thoroughly a the browser spec level. This seems roughly in line with the a proposal around lazy definitions: WICG/webcomponents#782 and could arguably fit into a larger conversation around the browser allowing for lazy upgrades, which would be somewhat similar to what this protocol looks to support.
Regardless, if you're SSR'ing the entirety of an element that may not come into view, the ability to control when it reconciles that delivered SSR render with the client-side render sounds like a good idea.
|
||
## Use case 2: On-demand hydration | ||
|
||
Hydration can be deferred until some data or user-driven signal, such as interacting with an element. In this case server-rendering is configured to add `defer-hydration` to all elements so that nothing will automatically hydrate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
visibility
is such a hot/important topic for upgrade/hydration that we should probably include it in line with such as interacting with an element
.
|
||
# Background | ||
|
||
In server-side rendered (SSR) applications, the process of a component running code to re-associate its template with the server-rendered DOM is called "hydration". The defer-hydration protocol is design to allow controler over hydration to solve two related problems: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple minor grammatical point(s)
change from
The defer-hydration protocol is design to allow controler over hydration to solve two related problems:
to
The defer-hydration protocol is
designdesigned to allowcontrolercontrol over hydration to solve two related problems:
|
||
# Overview | ||
|
||
The Defer Hydration Protocol specifies an attribute named `defer-hydration` that is placed on elements, usually during server rendering, to tell them not to hydrate when they are upgraded. Removing this attribute is a signal that the element should hydrate. Elements can observe the attribute being removed via `observedAttribute` and `attributeChangedCallback`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious on folks perspective around defer-hydration
as compared to something akin to loading="lazy"
If the attribute was, say, hydration
, with options like:
defer
: Hydrates based on internal behaviorlazy
: Hydrates based on when in viewporteager
: Default behavior
Should the attribute control both the intended behavior and the current state? Some tools use :visible
, Stencil uses the hydrated
class for hydrated state.
Decoupling this and setting a convention around the intended behavior and leaving how state is communicated up to the tools might be helpful.
an additional behavior that might be nice to have is |
I saw that 11ty had implemented this proposal so I wanted to reiterate my questions. What is missing from this proposal is a concrete use-case. What is the use-case where a component should delay its hydration because a parent told it to? What is the parent needing to do to the component before it hydrates? A good use-case would be something like a special type of component that is tightly coupled to its parent. Point (2) specifies data dependencies, but that is not what I would call a specific use-case. |
The biggest motivation for this proposal is to ensure top-down initialization of SSR'ed elements. In client-side code, shadow children are necessarily created after their parents. So they will have received properties from their parents and can assume that they can fire events that will be handled by their parents. With SSR'ed HTML and shadow roots elements are already created and will initialize in definition order. This can cause three major problems:
In addition to those problems that defer-hydration solves, it also creates an interoperable signal for islands-type architectures to use to trigger components to hydrate by removing the attribute on visibility, interaction, etc,. |
Thanks! I think I see 2 use-cases here. 1 is a tight coupling between components that are not serialized in such a way that they could be hydrated independently. The second is the island-control use-case. I'd like to skip the island idea for now and focus on the first idea because it's the one I understand the least. Let's put this to real-code. Let's say you have this: <parent-1>
<template shadowroot="open">
<child-1></child-1>
</template>
</parent-1> Correct me if I'm wrong, but I think you are saying that If I'm understanding this correctly, then this can be fixed by serializing <parent-1>
<template shadowroot="open">
<child-1 first-name="Matthew"></child-1>
</template>
</parent-1> So my next question is why wouldn't you do it this way? I think I'm missing something, and there's a reason why you can't just serialize. |
This is indeed a partial solution, but it only works with easily serializable primitive data for e.g. leaf widgets, and breaks down with more complex application data:
This protocol is attempting to help provide a robust SSR capability for Web Components in the general case. If a given SSR implementation knew all of the data for a given WC instance was serializable to attributes, it could opt to let the instance eagerly hydrate and not use Thus, this protocol is addressing cases where that's not possible/practical. |
Thanks! So the use-case is passing complex types. Are there examples of generic components that would need complex types? Are there examples of native elements that need complex types? My current thinking is that this use-case is for tightly coupled "application logic" components. These types of components are contained to within a single codebase. Perhaps cross-team within a company. But not a generic component that you might distribute to just anyone. With that being the case I don't think a community standard is necessary here. If After thinking about it more, my general take is that delaying hydration is bad and should be avoided when possible. Doing something like this within your own application because you value the developer convenience of passing complex objects is completely fine, but it's not something we should endorse. |
As I've been playing around with this proposal, one thing which has bothered me is the fact that you need to put <my-outer-component defer-hydration>
<template shadowroot="open">
<!-- I *MUST* put `defer-hydration` here too, or else it will hydrate immediately. -->
<my-inner-component defer-hydration></my-inner-component>
</template>
</my-outer-component> Since I couldn't think of a good way to do this, but I recently found out about custom state pseudo-classes which almost implement this. The key thing about pseudo-classes is that they inherit down the tree just like CSS does. This means that a component at the bottom of the render tree can detect what its parent's state is. If we define it as custom state like <my-outer-component defer-hydration>
<template shadowroot="open">
<!-- No `defer-hydration` attribute here! -->
<my-inner-component></my-inner-component>
</template>
</my-outer-component> class MyOuterComponent extends HTMLElement {
constructor() {
super();
// Set the custom state based on the `defer-hydration` attribute.
const internals = this.attachInternals();
if (this.hasAttribute('defer-hydration')) internals.states.add('--defer-hydration');
}
}
customElements.define('my-outer-component', MyOuterComponent);
class MyInnerComponent extends HTMLElement {
constructor() {
super();
// Only hydrate if the custom state is not set.
const defer = this.matches(':host-context(:--defer-hydration)');
if (!defer) this.hydrate();
}
// ...
}
customElements.define('my-inner-component', MyInnerComponent); And the inner component is able to defer based on the parent's
I'm not sure if this is really a good direction for Edit: In retrospect, you can do a lot of this with plain Number 4. is actually a bit worse, since there's no clear indicator of when the relevant selector might change for class MyComponent extends HTMLElement {
constructor() {
super();
if (!this.matches(':host-context([defer-hydration])')) {
this.hydrate();
} else {
this.onMatchChange(':host-context([defer-hydration])', () => {
this.hydrate();
});
}
}
// ...
}
customElements.define('my-component', MyComponent); To hydrate effectively, that API still needs to call back synchronously, and given that any DOM manipulation could result in changing match state, that means a lot of APIs could now synchronously cause an element to hydrate, which might be difficult to manage from an implementation perspective. |
@matthewp I'd challenge you to imagine a future where Another angle here is that, even with "application-specific" components, there is immense value in the flexibility that web components provide in the ability to incrementally migrate a given component or codebase from one WC base class to another; protocols like this that define any extra interface points above the standard spec'ed interface ensure that's also possible. If the extent of your vision for web components stops at |
@dgp1130 That would be a pretty clever approach, but unfortunately besides the defacto limitations you mentioned with |
@kevinpschaaf Thanks for some more specific use-cases. I can imagine these types of components as I'm the author of one myself. Mine does not support server rendering at the moment. If it did I can imagine I would infer if I needed to render defaults. You can do this easily with declarative shadow DOM by checking if
This protocol is recommending this approach for all components. From the proposal: "1. Interoperable incremental hydration across web components. Components should not automatically hydrate upon being defined, but wait for a signal from their parent or other coordinator." This is the source of our disagreements. Components absolutely should hydrate upon being by defined. Only in very special circumstances should they not do so. The fact that custom elements can hydrate independent of a tree is the biggest benefit of web components. Proposing that they not do so, and instead way for some root element to first hydrate makes them more like a React tree. We should not make exceptional scenarios the default. Keep in mind that Eleventy/webc has implemented this proposal, so they are conforming to what is in my opinion a bad default, right now. |
My biggest issue with this proposal is that it is a blanket recommendation that all components defer to the wishes of a parent component on when to hydrate. A element should always decide for itself when to hydrate. This is a case-by-case basis. Some native elements have similar constraints, but they always define an API in which they are in charge and the user only provides a hint. For example
Note the important part as defined by the browser. That is, the browser defines when to take action, it doesn't wait for the application developer to tell it that it's time. |
I don't follow.
I see. In general it should be possible for
Sorry, but I don't think this follows. If we can agree that an element shouldn't hydrate until it is fully configured (for performance reasons, amongst others discussed), this is never something a component can decide for itself. A component cannot know when or if a host will or has configured it, given it's just as valid for a host to do nothing to an element as to set a bunch of elements/properties. In the general case, this is only knowable by the host. |
@kevinpschaaf, I wasn't aware of implementation issues with <my-component style="--defer-hydration: true">
<my-child-component></my-child-component>
</my-component> class MyChildComponent extends HTMLElement {
constructor() {
super();
const defer = getComputedStyle(this).getPropertyValue('--defer-hydration')!.trim() === 'true';
if (!defer) this.hydrate();
}
}
customElements.define('my-child-component', MyChildComponent); Definitely a hack and doesn't help with listening for changes to the
I think you could still support this use case by actually writing <my-parent-component defer-hydration>
<!-- Loads as soon as parent's `defer-hydration` is removed. -->
<my-component></my-component>
<!-- Still deferred when parent's defer-hydration is removed.
`my-parent-component` can manually remove this `defer-hydration` when ready. -->
<my-lazy-component defer-hydration></my-lazy-component>
</my-parent-component> This does require knowing which components will be hydrated lazily during render time, but I guess we're already assuming that for Alternatively if hydration happens top down, when |
@dgp1130 Yeah, something like a StyleObserver would be needed to do this robustly, which doesn't exist yet.
An issue here is that Thus in the general case, I think it's always going to be safest to apply |
Yes, I've talked before about how import order naturally causes bottom-up hydration and this can make certain tasks more difficult. However, I think This does imply that <my-a>
<template shadowroot="open">
<div defer-hydration>
<my-b>
<template shadowroot="open">
<my-c></my-c>
</template>
</my-b>
</div>
<div><!-- Other stuff in my-a --></div>
</template>
</my-a> In this example, I don't mean to distract too much from the overall |
I think merging this is a huge mistake. I have disagreed with some of the proposals in this repo but this is the only one that I feel is actually harmful. As stated above, this proposals goal is to abstract away the difference between client-side rendering and hydrated rendering. It does so by making hydrated rendering slower. It's perfectly fine for a framework to chose this trade-off, but making it a recommendation for the community to also do so, is harmful. |
As a reminder the "Proposal" status level that is suggested as the next step for this conversation is outlined as follows:
For anyone with continuing concerns about the current shape of the proposal, leveraging issues or PRs for modifications is a great way to follow up on those, or if your concern is even deeper, then you might find submitting an alternate proposal that you felt addressed the concepts herein in a more palatable manner productive to your cause. |
For frameworks to choose this trade-off they need a specification to interop over. This is that specification. It's entirely opt-in, and once again the We've determined on the Lit project that this feature is required for proper hydration, and will use this feature. By publishing a protocol proposal here we are making it possible for others to interop with us in a formalized way. That is the point of community protocols. I strongly disagree with your assessment of this protocol, but regardless of that there is room in community protocols for protocols that not everyone agrees with. There's even room for competing protocols. As long as they can be named, referenced, and are specified enough for multiple independent and compatible implementations this repository is serving a good purpose. |
That's not the point, if that was the case then everything would get merged. If this is truly the case then I have at least 1 proposal of my own that I'll be getting merged soon. But I don't think that you think this is the point of the repository, as there are no non-Lit involved proposals that have ever been merged. |
I think there should be a high bar for just blocking a proposal, especially because say that there needs to be two independent implementations to move to "accepted". You seem to want a veto here, but we haven't established anything like that in the process. Must we satisfy your objections to even make a Proposal even though we've made it very clear our rationale and the problems we already solve with this protocol?
Given your tone in this conversation, would you actually be doing this in good faith? Do you use the protocol today? Are you truly looking to interop with other components and libraries? Would you show up to WCCG meetings to discuss it? Because if so, then make the proposal! If you're just trying to gum up the works I think your energy would be better spent elsewhere. |
I haven't fully grokked the details of this proposal, but we (LWC) will probably be evaluating whether to implement it in the next couple months or so. I'd be really interested to understand better the concerns around this proposal, and I'm also still a little unclear about what would "count" as a framework-interoperable implementation (e.g. see my question on nitty-gritty details). Maybe this would be worth a breakout session for the WCCG? |
The other proposals/drafts that are currently being developed (context, task, slottables) all relate to the internal implementation of a component. I think this is part of what makes them so interoperable. I think that in this context, we need interoperability to not just mean interoperability with components that implement those protocols, but also interoperability with components that don't, which in general is most components. A component that doesn't implement a protocol shouldn't care about those that do (a property which I think this proposal maintains) and a component that does shouldn't care about those that don't (a property I don't think this proposal maintains). This proposal seems fundamentally different to the other proposals in that it seeks to apply behavior to an instance of a component from the outside. Only some web components will be written to support Much has been said about this being opt-in, but I'm not sure it's opt-in in quite the same way as the other protocols. It seems to me that if we seek to involve components that have not opted-in, we we will draw incorrect conclusions about the behavior of those components. But how do we ensure we only seek to involve components that have opted in? One idea I have is for components to have a But even so, if only some children support deferring hydration, and the others don't, then I feel like:
So I'm unsure what the true benefit of |
I overlooked this in my previous comment, but components have to opt-in to hydration in the first place, do they not? This might be a silly question, but how do we actually know whether we can safely SSR any given component? Is SSR interoperable? (And if not, how can |
@mattlucock All good points that I agree with. This being a proposal that component behavior is added from the outside is indeed unlike the others. As far as the opt-in part, I think that term is being used as a sort of cover for what is really going on. It's not actually opt-in at the component level, you can go read Lit source code and see that they are opting all Lit components in, full stop. See here. I think their hope is that they can convince enough other frameworks to do the same, then essentially a big chunk of custom elements in the wild will have been opted-in, even though the author did not make that decision on their own. It doesn't matter to them that some others will not have it, they are betting on the numbers being in their favor. |
I think I was a bit confused when reading the proposal a few weeks ago—although I think that's because the proposal is confusing. Reading it again, it's not apparent that a parent 'asks' its children to defer hydration like I thought; it more seems like a component would determine for itself that it should defer hydration when being SSR-d. But this actually just creates a different variation of the same problem: how does a child know whether its parent participates in the protocol and will remove the @matthewp It makes complete sense to me that a framework for SSR-ing web components could opt all components in to something like this by default; this thing would basically just be an implementation detail of components built using the framework. But as I was getting at, it feels like this only makes sense in the context of a particular framework, and it feels like this fundamentally cannot be made interoperable in any way that makes sense. I am concerned by the approach being taken by this proposal, which appears to be "we've implemented this in Lit, and now we're making this a community protocol so anyone can choose to interoperate with us if they want to". Not only does it presuppose that Lit's design decisions are the right ones and should determine the design of components broadly, but it also presupposes that these things can and should be interoperated with.
I think this is fundamentally the wrong approach. We can't build community consensus for design decisions if the design decisions have, apparently, already been made, and aren't really up for debate. 'Formalizing the design of Lit' is not what this group should be trying to achieve. |
I'm actually glad that @justinfagnani put the effort into documenting how Lit does things, so that other frameworks might be able to interoperate with Lit. This is kind of the whole goal of web components. Thinking more about this proposal, though, I feel like it may be putting the cart before the horse – before figuring out how 2 WC frameworks should hydrate together, we need to figure out how they'll SSR together. E.g. if LWC wanted to implement support for SSR'ing Lit components and hydrating with them, it's not enough to use Again, I'm not against this proposal, but I'd like to see some brave WC framework author figure out how to SSR/hydrate Lit components, and then it'd be clearer to me how this is supposed to work in practice. Alternatively: if this proposal could be distilled down into something non-hydration related (e.g. |
@nolanlawson The point I was trying to make is that "the whole goal of web components" is not, and cannot be, to "interoperate with Lit". I think I may be over-emphasizing this concern, but this framing and thinking is really unproductive for what is supposed to be a community initiative, and I also think it's backwards: it's not that web components should interoperate with Lit, but that Lit should interoperate with web components. I think I generally agree with the rest of your sentiment, but I am a bit pessimistic about there being a distilled form of this that can be truly interoperable. There is nuance to this, which I mentioned in my previous comment, in that this seeks to apply behavior to a component from the outside, only some components will support it, and if we try and apply it to a component that doesn't support it we will draw incorrect conclusions about that component's behavior. |
@mattlucock These are valid concerns. The goal here is definitely interop among multiple WC frameworks – not just Lit. However, Lit might be at the vanguard of figuring out this whole SSR/hydration thing, so I take their proposal seriously when they say that something like If it's not, then by all means, let's build something better! But I respect Lit for coming to this forum to at least reach out to other WC frameworks and try to figure out how to interoperate. |
One thing I just realized is that If I do: try {
el.removeAttribute('defer-hydration');
console.log('No error detected?');
} catch (err) {
console.log('Caught error!');
} On a component which throws an error, I get:
Stackblitz minimal reproduction. For this proposal, that means it is impossible to detect a hydration error from another component. Is this an intentional design choice or an oversight? I don't see the word "error" in this PR or its discussion. It is possible to workaround, though it's a bit hacky. We could do something like: declare global {
var lastHydrationError?: unknown;
}
class HydratingElement extends HTMLElement {
hydrate(): void {
throw new Error('Oh noes!');
}
static observedAttributes = ['defer-hyration'];
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
if (name !== 'defer-hydration' || newValue !== null) return;
// Clean up any pre-existing errors, in case they weren't caught.
delete globalThis.lastHydrationError;
try {
this.hydrate();
} catch (err) {
// If I `throw` here, no one will catch it, so I can only log the error instead.
globalThis.lastHydrationError = err;
}
}
}
customElements.define('hydrating-element', HydratingElement);
function hydrate(el: Element): void {
el.removeAttribute('defer-hydration');
// ^-- Won't `throw` if anything went wrong.
// Check `lastHydrationError`.
const error = globalThis.lastHydrationError;
// Clean up `lastHydrationError` so the next hydration doesn't pick it up.
delete globalThis.lastHydrationError;
// Propagate error to consumers.
throw error;
}
hydrate(document.querySelector('hydrating-element'));
// ^-- Throws as expected. This is certainly an awkward contract and I'm sure there are better ways to do it, but it's a trivial workaround which is able to propagate hydration errors. Unfortunately this would need to be baked into the protocol. Is it worth having a mechanism to detect hydration errors? Is there a better approach than what I'm describing? |
@dgp1130 I think what you're running into is called a "custom element callback reaction" error. It should be catchable with an error listener on the I've found this to be a generic problem any time you're trying to catch an error in |
@nolanlawson Thanks for that context and excellent blog post. I can definitely understand why Would it be feasible to use the
So more than just uncaught callback reaction errors will trigger it. Is there a risk of potentially catching errors which did not originate from an Fortunately First is that component or its dependencies may call Second is composed hydration. Take the following example of hydration "call stack": <outer-component>
<inner-component defer-hydration>
<error-component defer-hydration></error-component>
</inner-component>
</outer-component> In this case, we're using
Third is catching errors from class MyComponent extends HTMLElement {
static observedAttributes = ['defer-hydration', 'attr'];
attributeChangedCallback(name, _oldValue, newValue) {
if (name === 'defer-hydration' && newValue === null) {
this.onHydration();
} else {
throw new Error('Unknown attribute'); // Caught by `window` `error` callback.
}
}
onHydration() {
this.setAttribute('attr', '');
console.log('Hydration successful!'); // Logs as expected.
}
} That One other challenge is that multiple Does |
If you set the global |
Wouldn't that still fail for the third example I gave? Any error in an |
@dgp1130 IIUC yeah, if you have an |
These functions make it easy to hydrate specific elements as dependencies of a component. To discuss a few notable design decisions: `comp.hydrate` queries an element, meaning it's not possible to separate the query step from the hydration step. I don't like this, but for now it seems to be necessary. The obvious alternative would be `comp.query('some-comp').hydrate(SomeComp)`. That looks nice, but it begs the question: What does `comp.query('some-comp')` return? I don't want it to return `ElementRef<SomeComp>`, because the element is not yet hydrated and does not actually satisfy the contract of `SomeComp`. `ElementRef<Dehydrated<SomeComp>>` is more agreeable, but relies on typing for safety, meaning it doesn't help JavaScript users. We could allow `ElementRef<SomeComp>` but then throw on any `read` operation, but I don't like the idea of having an `ElementRef` instance you can't read, that feels like a design smell for a type that doesn't provide any abstraction. Instead my goal is to prevent `ElementRef<SomeDehydratedElement>` as a possible pattern, but this requires `comp.hydrate` to do the query step. `hydrate` and `comp.hydrate` both require the class definition of the element (`SomeElement`) as an input. This doesn't actually do anything, it only does an `instanceof` check to make sure the right class was given. However the point is to ensure that the custom element is properly defined prior to hydration. If we didn't do this, the `customElements.define` call might be moved *after* hydration and fail at runtime. This ensures bundlers always order the `customElements.define` call prior to any associated `hydrate` call. We're using `class SomeElement {}` as a proxy for `customElements.define('some-element', SomeElement);`, but it is the closest we can do here, and usually accurate as custom elements are typically defined in the top-level scope of the file containing their class definition. `hydrate` unfortunately does not throw an error if a component's hydration fails. This is due to the fact that `attributeChangedCallback` does not propagate its errors to whomever invoked `el.removeAttribute('defer-hydration')`. This is just the way that custom element reaction callbacks work. If I could have it propagate errors I would, but this doesn't seem possible right now without encoding special behavior into the `defer-hydration` community protocol. I started [a discussion(webcomponents-cg/community-protocols#15 (comment))] on this topic, but until a relationship is formalized, I don't think there is a way to support propagating hydration errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed as per the March monthly WCCG meeting that merging this PR is appropriate, being it targets the "Proposal" status, the lowest possible level. We'll do our best to capture any conversations into issues for follow-up as the protocol moves up the various status levels (or not), so feel free to get your last comments posted over the next week and a half. On the 25th of March this PR will be merged as a "Proposal" protocol.
No description provided.