I am trying to integrate Vite SSR with MUI and I am facing warning and errors when I run the "dev:server" command.
Expected server HTML to contain a matching
in
Hydration failed because the initial UI does not match what was rendered on the server.
This is my codebase
entry-client.tsx
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { createBrowserRouter, matchRoutes, RouterProvider } from "react-router-dom";
import { routes } from "./App";
import createEmotionCache from "../../createEmotionCache";
import { CacheProvider } from "@emotion/react";
import { theme } from "../../theme";
import { ThemeProvider } from "@mui/material";
const cache = createEmotionCache();
async function hydrate() {
// Determine if any of the initial routes are lazy
let lazyMatches = matchRoutes(routes, window.location)?.filter(m => m.route.lazy);
// Load the lazy matches and update the routes before creating your router
// so we can hydrate the SSR-rendered content synchronously
if (lazyMatches && lazyMatches?.length > 0) {
await Promise.all(
lazyMatches.map(async m => {
let routeModule = await m.route.lazy!();
Object.assign(m.route, { ...routeModule, lazy: undefined });
}),
);
}
let router = createBrowserRouter(routes);
ReactDOM.hydrateRoot(
document.getElementById("app")!,
<React.StrictMode>
<CacheProvider value={cache}>
<ThemeProvider theme={theme()}>
<RouterProvider router={router} fallbackElement={null} />
</ThemeProvider>
</CacheProvider>
</React.StrictMode>,
);
}
hydrate();
entry-server.tsx
import type * as express from "express";
import * as React from "react";
import ReactDOMServer from "react-dom/server";
import { createStaticHandler, createStaticRouter, StaticRouterProvider } from "react-router-dom/server";
import { routes } from "./App";
import createEmotionCache from "../../createEmotionCache";
import createEmotionServer from "@emotion/server/create-instance";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { CacheProvider } from "@emotion/react";
import { theme } from "../../theme";
export function createFetchRequest(req: express.Request): Request {
let origin = `${req.protocol}://${req.get("host")}`;
// Note: This had to take originalUrl into account for presumably vite's proxying
let url = new URL(req.originalUrl || req.url, origin);
let controller = new AbortController();
req.on("close", () => controller.abort());
let headers = new Headers();
for (let [key, values] of Object.entries(req.headers)) {
if (values) {
if (Array.isArray(values)) {
for (let value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
let init: RequestInit = {
method: req.method,
headers,
signal: controller.signal,
};
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = req.body;
}
return new Request(url.href, init);
}
export async function render(request: express.Request) {
const cache = createEmotionCache();
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);
let { query, dataRoutes } = createStaticHandler(routes);
let remixRequest = createFetchRequest(request);
let context = await query(remixRequest);
if (context instanceof Response) {
throw context;
}
let router = createStaticRouter(dataRoutes, context);
const html = ReactDOMServer.renderToString(
<CacheProvider value={cache}>
<ThemeProvider theme={theme(request)}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<StaticRouterProvider router={router} context={context} nonce="the-nonce" />
</ThemeProvider>
</CacheProvider>,
);
const emotionChunks = extractCriticalToChunks(html);
const emotionCss = constructStyleTagsFromChunks(emotionChunks);
return {
html,
css: emotionCss,
};
}
server.ts
import type { Request, Response, NextFunction } from "express";
import fs from "fs/promises";
import path from "path";
import express from "express";
import compression from "compression";
import serveStatic from "serve-static";
import { createServer as createViteServer } from "vite";
const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD;
const resolve = (p: string) => path.resolve(__dirname, p);
const getStyleSheets = async () => {
try {
const assetpath = resolve("dist/assets");
const files = await fs.readdir(assetpath);
const cssAssets = files.filter(l => l.endsWith(".css"));
const allContent = [];
for (const asset of cssAssets) {
const content = await fs.readFile(path.join(assetpath, asset), "utf-8");
allContent.push(`<style type="text/css">${content}</style>`);
}
return allContent.join("\n");
} catch {
return "";
}
};
async function createServer(isProd = process.env.NODE_ENV === "production") {
const app = express();
// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
// can take control
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
logLevel: isTest ? "error" : "info",
});
// use vite's connect instance as middleware
// if you use your own express router (express.Router()), you should use router.use
app.use(vite.middlewares);
const requestHandler = express.static(resolve("assets"));
app.use(requestHandler);
app.use("/assets", requestHandler);
if (isProd) {
app.use(compression());
app.use(
serveStatic(resolve("dist/client"), {
index: false,
}),
);
}
const stylesheets = getStyleSheets();
app.use("*", async (req: Request, res: Response, next: NextFunction) => {
const url = req.originalUrl;
try {
// 1. Read index.html
let template = await fs.readFile(isProd ? resolve("dist/client/index.html") : resolve("index.html"), "utf-8");
// 2. Apply Vite HTML transforms. This injects the Vite HMR client, and
// also applies HTML transforms from Vite plugins, e.g. global preambles
// from @vitejs/plugin-react
template = await vite.transformIndexHtml(url, template);
// 3. Load the server entry. vite.ssrLoadModule automatically transforms
// your ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
let productionBuildPath = path.join(__dirname, "./dist/server/entry-server.mjs");
let devBuildPath = path.join(__dirname, "./src/client/entry-server.tsx");
const { render } = await vite.ssrLoadModule(isProd ? productionBuildPath : devBuildPath);
// 4. render the app HTML. This assumes entry-server.js's exported `render`
// function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const appHtml = await render(req);
console.log('appHtml', appHtml)
const cssAssets = appHtml.css;
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(`<!--app-html-->`, appHtml.html).replace(`<!--head-->`, cssAssets);
// 6. Send the rendered HTML back.
res.status(200).set({ "Content-Type": "text/html" }).end(html);
} catch (e: any) {
!isProd && vite.ssrFixStacktrace(e);
console.log(e.stack);
// If an error is caught, let Vite fix the stack trace so it maps back to
// your actual source code.
vite.ssrFixStacktrace(e);
next(e);
}
});
const port = process.env.PORT || 7456;
app.listen(Number(port), "0.0.0.0", () => {
console.log(`App is listening on http://localhost:${port}`);
});
}
createServer();
For some reason, I am unable to run "dev:server" on stackblitz.
I am trying to integrate Vite SSR with MUI and I am facing warning and errors when I run the "dev:server" command.