vitejs / vite

Next generation frontend tooling. It's fast!
http://vite.dev
MIT License
69.07k stars 6.24k forks source link

Style injection conflicts with SSR hydration on document #15765

Open swwind opened 10 months ago

swwind commented 10 months ago

Describe the bug

tl;dr: vite dev injected <style> tags maybe removed by preact's hydration on <html> element.

Actually my SSR framework renders the whole <html> tag and I was trying to figure out how to deal with the styles in dev server.

Suppose I have a project like this:

// App.tsx
import "./app.css"; // any styles
export function App() {
  return <html>
    <head> ... </head>
    <body> ... </body>
  </html>
}
// entry.tsx
import { render } from 'preact';
import { App } from './App.tsx';
render(<App />, document, document.documentElement);

And in server side, I just have something like this:

import { render } from 'preact-render-to-string';
import { App } from './App.tsx';
// when request coming
const html = render(<App />); // <html><head>...</head><body>...</body></html>
return new Response('<!DOCTYPE html>' + html);

The code above could work for production build, as I can read the manifest file and know which entry requires which style and inject them into the final html code. However, when I was using dev server, I don't know how many styles there in the project, and I can only rely on vite's dynamic style injection. Here comes the problem.

To describe the problem more accurately, let's think it step by step after open browser:

  1. client start import "./entry.tsx"
  2. continue import "./App.tsx"
  3. continue import "./app.css", the injection code __vite__updateStyle runs, a style tag injected to head
  4. the hydrate function in "./entry.tsx" runs and everything just added got removed

After these steps, no styles can get rendered.

Reproduction

n/a

Steps to reproduce

No response

System Info

all latest version

Used Package Manager

pnpm

Logs

No response

Validations

swwind commented 10 months ago

Actually I just found I can manually save those injected styles before hydration and inject them after hydration.

function hydrate(vnode: VNode) {
  let injections: NodeListOf<HTMLStyleElement> | null = null;
  if (import.meta.env.DEV) // save before hydration
    injections = document.head.querySelectorAll("style[data-vite-dev-id]");
  render(vnode, document, document.documentElement);
  if (import.meta.env.DEV && injections) // inject after hydration
    injections.forEach((element) => document.head.appendChild(element));
}

Don't know if there are any side effects, but it seems to work well.

sapphi-red commented 9 months ago

I'm not sure if this is something Vite can fix. It seems a fix is included in a canary react. https://remix.run/docs/en/main/future/vite#styles-disappearing-in-development-when-document-remounts

swwind commented 9 months ago

I'm not sure if this is something Vite can fix. It seems a fix is included in a canary react. https://remix.run/docs/en/main/future/vite#styles-disappearing-in-development-when-document-remounts

Thanks for commenting. Although I think this should be an expected behavior for react/preact as re-mount of html element should remove every other elements that should not exist. In another word, we should not let react to fix it, this should be remix's work when hydrating.

I was expecting vite to expose something inside @vite/client such as the sheetsMap variable or some remountStyles() function for us to manage injected styles manually. (Although the solution I provided before is not unusable)

sapphi-red commented 9 months ago

In another word, we should not let react to fix it

I think it cannot be handled by a meta-framework side completely because it seems to affect many examples (https://github.com/facebook/react/issues/24430).

I was expecting vite to expose something inside @vite/client such as the sheetsMap variable or some remountStyles() function for us to manage injected styles manually. (Although the solution I provided before is not unusable)

Perhaps Vite can provide a helper for that if it's a general problem among many rendering frameworks (e.g. React, Vue, Svelte).

import.meta.hot.runWithoutStyles(() => {
  hydrate(<App />, document)
})