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 Issues #5

Open
mayerraphael opened this issue Nov 30, 2022 · 6 comments
Open

Hydration Issues #5

mayerraphael opened this issue Nov 30, 2022 · 6 comments

Comments

@mayerraphael
Copy link

mayerraphael commented Nov 30, 2022

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.

@halodevcr
Copy link

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

@mayerraphael
Copy link
Author

mayerraphael commented Feb 24, 2023

@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.

@halodevcr
Copy link

halodevcr commented Feb 24, 2023

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

@mayerraphael
Copy link
Author

mayerraphael commented Feb 25, 2023

@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

@igorfiquene
Copy link

igorfiquene commented Apr 22, 2023

@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.

@mayerraphael
Copy link
Author

@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.

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