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

Vue Teleport Support #173

Open
michaelkplai opened this issue Jul 11, 2022 · 9 comments
Open

Vue Teleport Support #173

michaelkplai opened this issue Jul 11, 2022 · 9 comments

Comments

@michaelkplai
Copy link

I wanted to ask about support for Vue’s Teleport Component. Currently, using teleport leads to a hydration error.

Teleport SSR considerations have been documented here. Teleport content is exposed in the Vue context object and must be manually injected into the HTML.

I think this can be implemented by adding a teleports field to DocParts and adjusting buildHtmlDocument to inject the teleport content.

In the beginning we can support only teleporting to body like Nuxt does. Additional teleport support could be added (full CSS selectors), but it would require a DOM parser or clever regex.

Please let me know if this is a feature we would be interested in adding. I’d be happy to look into creating a PR.

@michaelkplai
Copy link
Author

I’ve got a working local pnpm patch based on this Nuxt PR.

Misc. notes:

  • Client-rendered body teleports are added to the bottom of . Oddly, server-rendered teleports must be added to the top of to prevent hydration mismatches.
  • Cheerio is an option to support full CSS selectors, however, it seems heavy (500+kB).
  • A compromise to full CSS selectors could be only allowing teleports to empty elements with only ID attributes. This would let us search and replace using <div id=“…”></div>. This wouldn’t be a huge compromise since Vue recommends dedicated containers

@MichealPearce
Copy link

Do you have a example of it causing a hydration error? I currently use Teleport perfectly fine within my vite-ssr app.

Is the element you're teleporting to rendered by Vue or outside of it like in the raw html?

@michaelkplai
Copy link
Author

michaelkplai commented Jul 14, 2022

I am teleporting to an element outside of Vue in the raw html.

I’ve created a simple reproduction on top of the vitesse template.

Oddly enough I’m not getting a hydration error. However, the teleported elements are not being rendered on the server (verified using view Page Source).

@phlegx
Copy link

phlegx commented May 18, 2023

Has the problem been solved in the meantime?

@phlegx
Copy link

phlegx commented Jun 8, 2023

@michaelkplai can you share please an example?

@phlegx
Copy link

phlegx commented Jun 8, 2023

I have found a test in vue source code: ssrTeleport.spec.ts

And here we see how vite-ssr does renderToString: src/vue/entry-server.ts#LL68C24-L68C38

And here the vue core docs about SSR and teleport: https://vuejs.org/guide/scaling-up/ssr.html#teleports

And here the nuxt code with html part bodyPrepend: renderer.ts#L308

And here an example: server.js

So, the problem adding a bodyPrepend to SSR is, that the teleport are included twice in client (server side rendered teleports and client side teleports).

@michaelkplai
Copy link
Author

I haven't looked at this problem in the last year and have since migrated away from the library. I created a reproduction previously:

reproduction on top of the vitesse template.

Here's the pnpm patch I used before:

[email protected]

diff --git a/core/entry-server.js b/core/entry-server.js
index b4a4439f779ac6aa0c75960dd3e855797c75c50a..0d5c97420b0c6ad0e9b78b8403ebb2c261cc5398 100644
--- a/core/entry-server.js
+++ b/core/entry-server.js
@@ -52,6 +52,9 @@ export const viteSSR = function viteSSR(options, hook) {
                 htmlParts.headTags += renderPreloadLinks(htmlParts.dependencies);
             }
         }
+
+        htmlParts.teleports = context.teleports || {}
+
         return {
             html: buildHtmlDocument(template, htmlParts),
             ...htmlParts,
diff --git a/utils/html.js b/utils/html.js
index 9f8f2156aed52847626f1a2e934be8fa39441754..3694f165230ee7428dde8546fbc6e1ecb7e0c523 100644
--- a/utils/html.js
+++ b/utils/html.js
@@ -22,7 +22,7 @@ export function renderPreloadLinks(files) {
 // @ts-ignore
 const containerId = __CONTAINER_ID__;
 const containerRE = new RegExp(`<div id="${containerId}"([\\s\\w\\-"'=[\\]]*)><\\/div>`);
-export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, body, initialState }) {
+export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, body, initialState, teleports }) {
     // @ts-ignore
     if (__DEV__) {
         if (template.indexOf(`id="${containerId}"`) === -1) {
@@ -33,7 +33,7 @@ export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, bo
         template = template.replace('<html', `<html ${htmlAttrs} `);
     }
     if (bodyAttrs) {
-        template = template.replace('<body', `<body ${bodyAttrs} `);
+        template = template.replace('<body>', `<body ${bodyAttrs}>${ teleports.body || '' }`);
     }
     if (headTags) {
         template = template.replace('</head>', `\n${headTags}\n</head>`);

@phlegx
Copy link

phlegx commented Jun 9, 2023

Like described in the official documentation of vue, we should consider to use an own DOM node:

Avoid targeting body when using Teleports and SSR together - usually, will contain other server-rendered content which makes it impossible for Teleports to determine the correct starting location for hydration. Instead, prefer a dedicated container, e.g. <div id="teleported"></div> which contains only teleported content.

We have two possible solutions:

  1. Teleport components are rendered only on client side (no hydration node mismatch).
<ClientOnly>
  <Teleport to="body">
    ...
  </Teleport>
</ClientOnly> 
  1. Required SSR rendering of teleport components, by using a unique DOM node outside app node (solves hydration node mismatch).
<Teleport to="#teleported">
  ...
</Teleport>

with index.html like:

...
<body>
  <div id="app"></div>
  <div id="teleported"></div>
</body>

SSR rendering does something like:

teleported = context.teleports['#teleported']
...
// Replace <div id="teleported"> with <div id="teleported" data-server-rendered="true">${teleported}

This is a solution for body teleports and SSR only!

@phlegx
Copy link

phlegx commented Jun 12, 2023

@michaelkplai please take a look at PR #207

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

No branches or pull requests

3 participants