sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.12k stars 1.84k forks source link

Custom Element rendered markup is removed during hydration #9772

Open dritter opened 1 year ago

dritter commented 1 year ago

Describe the bug

At work we use a couple of webcomponents (custom elements) from different teams. We chose webcomponents to be framework agnostic so they work everywhere. If these webcomponents render markup that markup gets removed on the client side when svelte takes over control. This does even happen, if I mark this component as custom element in vite (= has no effect, thus not included in reproducer):

diff --git a/vite.config.js b/vite.config.js
index bbf8c7d..0a479e1 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -2,5 +2,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
 import { defineConfig } from 'vite';

 export default defineConfig({
-   plugins: [sveltekit()]
+   plugins: [sveltekit({
+       template: {
+           compilerOptions: {
+               isCustomElement: (tag) => tag.startsWith('my-component')
+           }
+       }
+   })]
 });

Pointers in reproducer

The custom element gets defined in app.html and is used in +page.svelte.

Workaround

It works, if I wrap the component in @html (I guess that disables the hydration):

{@html '<my-component></my-component>'}

Maybe that could be done automatically if svelte detects a custom element?

Other tries

I tested this even with https://github.com/sveltejs/svelte/pull/8457 and it still happens. It would be nice, if this case could be considered in this PR.

Reproduction

  1. git clone https://github.com/dritter/sveltekit-custom-element-reproducer
  2. cd sveltekit-custom-element-reproducer
  3. npm i
  4. npm run dev -- --open
  5. Observe that the red element gets removed as soon as the client side takes over

Bildschirmaufzeichnung vom 2023-04-19, 09-54-12.webm

Logs

No response

System Info

System:
    OS: Linux 6.2 Arch Linux
    CPU: (8) x64 Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
    Memory: 2.87 GB / 15.39 GB
    Container: Yes
    Shell: 5.9 - /usr/bin/zsh
  Binaries:
    Node: 19.9.0 - /usr/bin/node
    npm: 8.19.2 - /usr/bin/npm
  Browsers:
    Chromium: 112.0.5615.165
    Firefox: 112.0.2
  npmPackages:
    @sveltejs/adapter-auto: ^2.0.0 => 2.0.0 
    @sveltejs/kit: ^1.5.0 => 1.15.5 
    svelte: ^3.54.0 => 3.58.0 
    vite: ^4.2.0 => 4.2.1

Severity

annoyance

Additional Information

No response

dummdidumm commented 1 year ago

The problem is that Svelte during hydration expects the DOM to look the same as when it was server-rendered. But the custom element already scaffolded itself, so Svelte trashes it. That makes sense for normal elements because it aligns Svelte and the DOM, but in case of custom elements ... tricky.

Another workaround is to defer registration of the custom element a bit until Svelte has finished hydrating.

dritter commented 1 year ago

Thanks for looking into this @dummdidumm

Defering the registration of the custom element is unfortunately not an option, because of two reasons:

  1. We want to keep framework agnostic, to avoid them being cluttered with if (isFrameworkA()) { ... } elseif (isFrameworkB()) { ... } code.
  2. Even if we would take this option, we would not know for how long we should defer that registration. I sketched a clumsy setTimeout() Version, but that would be a guessed value, that differs from device to device. It may be deferrable when using the custom element in an onMount() hook, but that would lead to my initial workaround ({@html ...}).