nextjs-stenciljs-ssr-example
nextjs-stenciljs-ssr-example copied to clipboard
Hydration Issues
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.
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 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.
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 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
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.
If you also want better SSR support with Stencil, please upvote my feature request at stencil: https://github.com/ionic-team/stencil/issues/4010
@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
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.
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.
@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.