nextjs-stenciljs-ssr-example icon indicating copy to clipboard operation
nextjs-stenciljs-ssr-example copied to clipboard

Hydration Issues

Open mayerraphael opened this issue 2 years ago • 6 comments

Warning: Did not expect server HTML to contain a <div> in <scoped-example>.
scoped-example
ReactComponent@webpack-internal:///../../packages/component-library-react/dist/react-component-lib/createComponent.js:28:13
ScopedExample
div
Index@webpack-internal:///./src/pages/index.tsx:14:62
App@webpack-internal:///./src/pages/_app.tsx:15:21
ErrorBoundary@webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:8:20742
ReactDevOverlay@webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:8:23633
Container@webpack-internal:///./node_modules/next/dist/client/index.js:70:24
AppContainer@webpack-internal:///./node_modules/next/dist/client/index.js:216:20
Root@webpack-internal:///./node_modules/next/dist/client/index.js:403:21

This is known with WebComponents and any form of hydration.

StencilJS hydrate removes the Slot Elements and places them inside the component and renders them. Now in your NextJS page, you have the old <SlotExample>-SLOT CONTENT-</SlotExample> tag.

So it does not match when NextJS is hydrating the WebComponent.

Currently there is no way around that problem. You maybe just don't get the error message because of the PRODUCTION env.

mayerraphael avatar Nov 30 '22 11:11 mayerraphael

I'm facing this issue you describing here @mayerraphael , have any progress or updates been done so that we can get around this problem?

I really appreciate any help

halodevcr avatar Feb 24 '23 17:02 halodevcr

@halodevcr there is. just run defineCustomElements not inside a react component (like he does in _app.tsx) but add it as an custom script tag to head so it runs before nextjs hydrates. stencil will rewrite the components back to their shadow dom form and react stops complaining as the fiber matches the dom again.

if you dont get it working just write me again. i can create a working sample.

mayerraphael avatar Feb 24 '23 18:02 mayerraphael

I would highly appreciate the working sample @mayerraphael , I have replicated the whole thing locally but I'm still getting the "Error: Hydration failed because the initial UI does not match what was rendered on the server."

Could the latest version of next be the issue ? I noticed this example is using 12 and mine 13

halodevcr avatar Feb 24 '23 21:02 halodevcr

@halodevcr Please keep in mind that this only works with components that use the Shadow DOM. LightDOM only components do not work with Hydrate, as they are never correctly resolved afterwards.

The problem with components without Shadow DOM is that they are rendered by the Hydrate package, but the internal DOM of the component is never hidden once stencil hydrates, as there is no shadow dom. So there is a break between what you specified in your NextJS/React component and what exists after stencil renders.

Also keep in mind that stencil adds the hydrated class, which will still produce a hydration warning(as it was not there before), but does not break anything. its just a warning, compared to hydration errors you get if nodes differ.

Get it working

First i upgraded NextJS and React to the latest version according to https://nextjs.org/docs/upgrading

pages/index.tsx

import { useState } from 'react';
import {
  ShadowExample,
  SlotShadowExample,
} from 'component-library-react';

const Index = () => {
  const [counter, setCounter] = useState(0);

  return (
    <div className="hero">
      <h1 className="title">Next.js + Tailwind</h1>
      <div>
        <h2>{counter}</h2>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
      <h4>slot-shadow-example</h4>
      <SlotShadowExample>
        <div onClick={console.log}>-SLOT CONTENT-</div>
      </SlotShadowExample>
      <hr />
      <h4>shadow-example</h4>
      <ShadowExample first="Jag" last="Reehal"></ShadowExample>
    </div>
  );
};

export default Index;

When i start the example, i get the following ERROR

image

So we need to adjust two files.

server.js

const express = require("express");
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const hydrate = require('component-library/hydrate');

const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = process.env.PORT || 5001;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();


  server.use(function(req, _, next) {
    req.url = req.originalUrl.replace('/nextjs_custom_server/_next', '/_next');
    console.log(req.url)
    next(); // be sure to let the next middleware handle the modified request.
  });

  server.use("/assets/components", express.static("./node_modules/component-library"));

  server.get('/__nextjs_original-stack-frame', (req, res) => {
    handle(req, res);
  });

  server.get('/_next/*', (req, res) => {
    handle(req, res);
  });


  server.all('*', async (req, res) => {
    const parsedUrl = parse(req.url, true);
    const { pathname, query } = parsedUrl;

    const html = await app.renderToHTML(req, res, pathname, query);
    const renderedHtml = await hydrate.renderToString(html);
    res.end(renderedHtml.html);
  })

  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://${hostname}:${port}`);
  });
})

And the new _document.tsx, which hydrates stencil before nextjs/react hydration.

import { Html, Head, Main, NextScript } from 'next/document'
import Script from 'next/script'

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <Script type="module" strategy="beforeInteractive">
          {`
            import { defineCustomElements } from "/assets/components/loader/index.js";
            console.log("Hydrate stencil");
            defineCustomElements(window).then(() => console.log("done"));
          `}
        </Script>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

The result is a working example (without an hydration error, only a warning which we cannot resolve yet and it does not have any affect on anything).

The WebComponent was rendered successfully.

image

If you also want better SSR support with Stencil, please upvote my feature request at stencil: https://github.com/ionic-team/stencil/issues/4010

mayerraphael avatar Feb 25 '23 07:02 mayerraphael

@halodevcr Please keep in mind that this only works with components that use the Shadow DOM. LightDOM only components do not work with Hydrate, as they are never correctly resolved afterwards.

The problem with components without Shadow DOM is that they are rendered by the Hydrate package, but the internal DOM of the component is never hidden once stencil hydrates, as there is no shadow dom. So there is a break between what you specified in your NextJS/React component and what exists after stencil renders.

Also keep in mind that stencil adds the hydrated class, which will still produce a hydration warning(as it was not there before), but does not break anything. its just a warning, compared to hydration errors you get if nodes differ.

Get it working

First i upgraded NextJS and React to the latest version according to https://nextjs.org/docs/upgrading

pages/index.tsx

import { useState } from 'react';
import {
  ShadowExample,
  SlotShadowExample,
} from 'component-library-react';

const Index = () => {
  const [counter, setCounter] = useState(0);

  return (
    <div className="hero">
      <h1 className="title">Next.js + Tailwind</h1>
      <div>
        <h2>{counter}</h2>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
      <h4>slot-shadow-example</h4>
      <SlotShadowExample>
        <div onClick={console.log}>-SLOT CONTENT-</div>
      </SlotShadowExample>
      <hr />
      <h4>shadow-example</h4>
      <ShadowExample first="Jag" last="Reehal"></ShadowExample>
    </div>
  );
};

export default Index;

When i start the example, i get the following ERROR

image

So we need to adjust two files.

server.js

const express = require("express");
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const hydrate = require('component-library/hydrate');

const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = process.env.PORT || 5001;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();


  server.use(function(req, _, next) {
    req.url = req.originalUrl.replace('/nextjs_custom_server/_next', '/_next');
    console.log(req.url)
    next(); // be sure to let the next middleware handle the modified request.
  });

  server.use("/assets/components", express.static("./node_modules/component-library"));

  server.get('/__nextjs_original-stack-frame', (req, res) => {
    handle(req, res);
  });

  server.get('/_next/*', (req, res) => {
    handle(req, res);
  });


  server.all('*', async (req, res) => {
    const parsedUrl = parse(req.url, true);
    const { pathname, query } = parsedUrl;

    const html = await app.renderToHTML(req, res, pathname, query);
    const renderedHtml = await hydrate.renderToString(html);
    res.end(renderedHtml.html);
  })

  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://${hostname}:${port}`);
  });
})

And the new _document.tsx, which hydrates stencil before nextjs/react hydration.

import { Html, Head, Main, NextScript } from 'next/document'
import Script from 'next/script'

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <Script type="module" strategy="beforeInteractive">
          {`
            import { defineCustomElements } from "/assets/components/loader/index.js";
            console.log("Hydrate stencil");
            defineCustomElements(window).then(() => console.log("done"));
          `}
        </Script>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

The result is a working example (without an hydration error, only a warning which we cannot resolve yet and it does not have any affect on anything).

The WebComponent was rendered successfully.

image

If you also want better SSR support with Stencil, please upvote my feature request at stencil: ionic-team/stencil#4010

This example works only in shadow dom components? I have the same problem, I tried your solution but not working in scoped components ( light dom ) using stencil as web components.

igorfiquene avatar Apr 22 '23 05:04 igorfiquene

@halodevcr Please keep in mind that this only works with components that use the Shadow DOM. LightDOM only components do not work with Hydrate, as they are never correctly resolved afterwards.

This example works only in shadow dom components? I have the same problem, I tried your solution but not working in scoped components ( light dom ) using stencil as web components.

Literally the first sentences says that it only works with shadow dom components, as they are resolved to fragments which do not conflict with reacts' fibers.

mayerraphael avatar Apr 22 '23 07:04 mayerraphael