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

Hydration with shadow root doesn't work #52

Open
amthrock opened this issue Sep 21, 2020 · 2 comments
Open

Hydration with shadow root doesn't work #52

amthrock opened this issue Sep 21, 2020 · 2 comments

Comments

@amthrock
Copy link

If you register a component as a custom element with { shadow: true } then the code in the connectedCallback will try to run the hydrate function with the shadow root as the target node. This ends up rendering the entire component inside the shadow root while preserving, but not rendering, existing children (perhaps they're being treated like slot contents?) outside the shadow root which defeats the purpose of hydration.

I found this out while testing SSR with shadow root; I know you can't declaratively add a shadow root. But I was wondering if moving existing children of the custom element into the shadow root then attempting to hydrate would work? Is there risk of content reflow or style flashing? Is this simply just a limitation of custom elements that cannot be circumvented and therefore not in scope here?

Here's an example:

import { h, Fragment } from 'preact';
import { useState } from 'preact/hooks'
import register from 'preact-custom-element';

export const Counter = ({increment}) => {
	let [ count, setCount ] = useState(0)

	return (
		<>
			<p>Count: {count}</p>

			<button onClick={() => setCount(count + Number(increment))}>Increment by: {increment}</button>
			<button onClick={() => setCount(count - Number(increment))}>Decrement by: {increment}</button>
			<slot />
		</>
	)
}

register(Counter, 'my-counter', ['increment'], { shadow: true })
<my-counter increment="5" hydrate>
	<p>Count: 0</p>
	<button>Increment by: 5</button>
	<button>Decrement by: 5</button>
</my-counter>

These are the results when the script loads:

<my-counter increment="5" hydrate>
	#shadow-root
		<p>Count: 0</p>
		<button>Increment by: 5</button>
		<button>Decrement by: 5</button>
	<p>Count: 0</p>
	<button>Increment by: 5</button>
	<button>Decrement by: 5</button>
</my-counter>

This would be the desired results:

<my-counter increment="5" hydrate>
	#shadow-root
		<p>Count: 0</p>
		<button>Increment by: 5</button>
		<button>Decrement by: 5</button>
</my-counter>
@marvinhagemeister
Copy link
Member

There is certainly a big gap in the spec when it comes to doing SSR with shadow roots. Declaratively hydrating shadow roots is not possible without custom solutions.

The approach you listed is very similar to what skatejs is doing in their SSR implementation. Upon rendering on the Server they serialize the shadow DOM, wrap it with a special node and inject a script that places all children back into the actual shadow DOM root when run in the browser.

Whilst the serialization is out of scope for this repo, having am integrated way to hydrate nodes in this library sounds like an experiment worth doing. It could become tricky with multiple slots in play. To be honest I'm not sure if there is a way to avoid a re-layout or flashing of content. Usually moving nodes around will have that effect, but I wonder if a <template>-element can help here.

@amthrock
Copy link
Author

Perhaps one day we will get declarative shadow roots!

I did take a rudimentary shot at using a template and it works well. Admittedly SEO is something I don't have a lot of knowledge about but to my understanding this code would still be crawlable.

import { h, hydrate } from 'preact';
import { Counter } from './counter' // Counter component from original post sans register() call

export class Counter2 extends HTMLElement {
	get increment() {
		return Number(this.getAttribute('increment'))
	}

	constructor() {
		super()
	}

	connectedCallback() {
		const component = <Counter increment={this.increment} />
		const templateShadowRoot= this.querySelector('template[shadow-root]')

		const markupToHydrate = templateShadowRoot.content.cloneNode(true)
		templateShadowRoot.remove()
		this.attachShadow({ mode: 'open' })
		this.shadowRoot.appendChild(markupToHydrate)
		hydrate(component, this.shadowRoot) 
	}
}

customElements.define('my-counter2', Counter2)
<my-counter2 increment="5">
	<template shadow-root>
		<p>Count: 0</p>
		<button>Increment by: 5</button>
		<button>Decrement by: 5</button>
	</template>
</my-counter2>

Of course this doesn't get the benefits from using the register method (e.g. automatic observed attributes) but perhaps this approach could be modified and implemented in the register method?

I share your concern about it becoming tricky when slots are in play. I'm not sure how they could be handled in the template approach.

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

No branches or pull requests

2 participants