Skip to content
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

[lit-html - SSR] missing parent check for defer-hydration? #2946

Closed
daKmoR opened this issue May 24, 2022 · 4 comments · Fixed by #2952
Closed

[lit-html - SSR] missing parent check for defer-hydration? #2946

daKmoR opened this issue May 24, 2022 · 4 comments · Fixed by #2952
Assignees

Comments

@daKmoR
Copy link
Contributor

daKmoR commented May 24, 2022

Description

I am not sure why but I currently have a situation where marker.parentElement in experimental-hydrate.ts is null.

https://github.com/lit/lit/blob/main/packages/lit-html/src/experimental-hydrate.ts#L161-L164

when I check the code I see this

const parent = marker.parentElement!;
if (parent.hasAttribute('defer-hydration')) {
  parent.removeAttribute('defer-hydration');
}

adding check so that parent can not be null seems to make it work.

e.g.

   const parent = marker.parentElement!;
-  if (parent.hasAttribute('defer-hydration')) {
+  if (parent && parent.hasAttribute('defer-hydration')) {
     parent.removeAttribute('defer-hydration');
   }

Is that just a missing check? or is there a 0 chance that parentElement is ever null? If so I need to check why it's null in my case...

@aomarks
Copy link
Member

aomarks commented May 25, 2022

My current hypothesis is that this could happen if you were hydrating a shadow root with an immediate child that is a void (aka empty) element, such as <input> or any of these, and that has an attribute binding. For example:

render() {
  return html`<input max=${this.maxLen}>`;
}

Void elements are special because when the HTML parser encounters a child of one of them, it hoists the child up as a sibling, since those elements aren't supposed to have children.

In SSR, when an element has an attribute binding, we represent that with a child comment like <!--lit-node 0-->. But if that's in a void element, it will end up as a sibling, instead of a child. We account for this in one place in the hydrate code, but not in another (the place you mentioned).

Working on a repro to confirm.

@daKmoR
Copy link
Contributor Author

daKmoR commented May 25, 2022

indeed that was the element render function

  render() {
    return html`
      <p>Hello World</p>
      <input
        type="text"
        value=""
        class="shadow-input"
        @focusin=${this.handleFocusIn}
      />
    `;
  }

and it was confusing as removing @focusin made it work...

but as you now explained it - it totally makes sense 👍

aomarks added a commit that referenced this issue May 25, 2022
### Background

In SSR, when an element has an attribute binding, we insert a `<!--lit-node 0-->` comment to tell `hydrate()` about the attribute binding.

However, if that element was a *void* element, such as `<input>` or anything from [this](https://html.spec.whatwg.org/multipage/syntax.html#void-elements) list, then when the HTML parser encounters it, all of the children it has will be hoisted up as siblings.

For example:

```ts
render() {
  return html`<input max=${this.maxLen}>`;
}
```

Is SSR'd as:

```html
<input max="42"><!--lit-node 0--></input>
```

But parsed by the browser as:

```html
<input max="42" />
<!--lit-node 0-->
```

This means when `hydrate()` encounters a case like this, we need to check `previousSibling` instead of `parentElement`, to find the element to receive the attribute binding.

### Bug

We already accounted for void elements where we actually create the attribute parts: https://github.com/lit/lit/blob/ac356997351874706f8be235559c765861dce67d/packages/lit-html/src/experimental-hydrate.ts#L340

But we did not account for it where we remove the `defer-hydration` attribute: https://github.com/lit/lit/blob/ac356997351874706f8be235559c765861dce67d/packages/lit-html/src/experimental-hydrate.ts#L161

This actually only crashed `hydrate()` if the void element was an *immediate child of a shadow root*, because in every other case, `element.parentElement` would return *something*, even if it wasn't the correct node.

### Fix

We now also account for void elements when we remove the `defer-hydration` attribute in the same way that we already did for creating attribute parts.

Note as part of this I switched from `previousSibling` to `previousElementSibling`, so that we can assume `removeAttribute` is defined. Seems like this should be safe?

Hopefully fixes #2946

cc @daKmoR 

### Alternative idea

I also thought about an alternative way to handle void elements generally, which is to detect them up-front during SSR rendering, since they are a small fixed set of tag names, so that we get an explicit signal about whether we need to check the parent or the sibling during hydration, instead of doing the previousSibling -> parentElement fallback. See 66b702b for how that would look. This could potentially have slightly better performance during hydration, since it replaces a DOM call with a string check. Not sure it's worth it though, and I think it would be a breaking change because it changes the rendering scheme.
@aomarks
Copy link
Member

aomarks commented May 25, 2022

Fixed and released

@daKmoR
Copy link
Contributor Author

daKmoR commented May 26, 2022

thaankks 🙇‍♂️

updated and works as expected 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

2 participants