reactjsnode.jsreact-routerreact-router-domvite

VITE.JS SSR Not Working with react-router-dom


I been trying to create template with Vite SSR with react-router-dom for my requirements. i know Next JS is better alternative for Vite SSR.

heare i have some configs that i made for this template.

Vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'

export default defineConfig({
    plugins: [react()],
    ssr: {
        external: [
            'react-router-dom'
        ]
    },

    resolve: {
        alias: {
            "@App": path.resolve(process.cwd(), "src"),
            "@Components": path.resolve(process.cwd(), "src/Components"),
            "@Pages": path.resolve(process.cwd(), "src/pages"),
            "@Styles": path.resolve(process.cwd(), "src/Styles"),
            "@Public": path.resolve(process.cwd(), "public")
        },
    },
})

Src/App.tsx

import { BrowserRouter } from "react-router-dom";


function Application() {
    return (
        <BrowserRouter>
            HI
        </BrowserRouter>
    );
}

export default Application;

Console

enter image description here

How to Reproduce this template

yarn create vite

Select Others Select ssr-react Select typescript+swc

Replace src/App.tsx to upper file

Run

yarn dev

Tags

vite vitejs vitejs ssr react react-router react-router-dom ssr js dom vite config

I Expected to working React application with react-router-dom.


Solution

  • Vite SSR + React Router Dom is working with StaticRouter

    Follow steps to replicate template

    yarn create vite
    
    yarn add compression cross-env sirv express react-router-dom
    

    enter image description here

    Files

    Root/Server.js

    import fs from "node:fs/promises";
    import path from "path";
    import express from "express";
    import { createServer as createViteServer } from "vite";
    
    const isProduction = process.env.NODE_ENV === "production";
    const Port = process.env.PORT || 3000;
    const Base = process.env.BASE || "/";
    
    const templateHtml = isProduction
        ? await fs.readFile("./dist/client/index.html", "utf-8")
        : "";
    
        const ssrManifest = isProduction
        ? await fs.readFile("./dist/client/.vite/ssr-manifest.json", "utf-8")
        : undefined;
    
    const app = express();
    let vite;
    
    // ? Add vite or respective production middlewares
    if (!isProduction) {
        vite = await createViteServer({
            server: { middlewareMode: true },
            appType: "custom",
        });
    
        app.use(vite.middlewares);
    } else {
        const sirv = (await import("sirv")).default;
        const compression = (await import("compression")).default;
        app.use(compression());
        app.use(Base, sirv("./dist/client", {
            extensions: [],
            gzip: true,
        }));
    }
    
    // ? Add Your Custom Routers & Middlewares heare
    app.use(express.static("public"));
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    app.get("/api", (req, res) => {
        res.json({ message: "Hello World" });
    });
    
    // ? SSR Render - Rendring Middleware
    app.use("*", async (req, res, next) => {
    
        // ! Favicon Fix
        if (req.originalUrl === "/favicon.ico") {
            return res.sendFile(path.resolve("./public/vite.svg"));
        }
    
        // ! SSR Render - Do not Edit if you don't know what heare whats going on
        let template, render;
    
        try {
            if (!isProduction) {
                template = await fs.readFile('./index.html', 'utf-8');
                template = await vite.transformIndexHtml(req.originalUrl, template);
                render = (await vite.ssrLoadModule("/src/entry-server.tsx")).render;
            } else {
                template = templateHtml;
                render = (await import("./dist/server/entry-server.js")).render;
            }
    
            const rendered = await render({ path: req.originalUrl }, ssrManifest);
            const html = template.replace(`<!--app-html-->`, rendered ?? '');
    
            res.status(200).setHeader("Content-Type", "text/html").end(html);
        } catch (error) {
            // ? You can Add Something Went Wrong Page
            vite.ssrFixStacktrace(error);
            next(error);
        }
    });
    
    // ? Start http server
    app.listen(Port, () => {
        console.log(`Server running on http://localhost:${Port}`);
    });
    

    Root/src/entry-server.tsx

    import ReactDOMServer from "react-dom/server";
    import { StaticRouter } from "react-router-dom/server";
    
    import { Router } from "./router";
    
    interface IRenderProps {
        path: string;
    }
    
    export const render = ({ path }: IRenderProps) => {
        return ReactDOMServer.renderToString(
            <StaticRouter location={path}>
                <Router />
            </StaticRouter>
        );
    };
    

    Root/src/entry-client.tsx

    import ReactDOM from "react-dom/client";
    import { BrowserRouter } from "react-router-dom";
    
    import { Router } from "./router";
    
    ReactDOM.hydrateRoot(
        document.getElementById("app") as HTMLElement,
        <BrowserRouter>
            <Router />
        </BrowserRouter>
    );
    

    Root/src/router.tsx

    import { Routes, Route } from "react-router-dom";
    
    import { Home } from "./pages/Home";
    import { Other } from "./pages/Other";
    import { NotFound } from "./pages/NotFound";
    
    export const Router = () => {
        return (
            <Routes>
                <Route index element={<Home />} />
                <Route path="/other" element={<Other />} />
                <Route path="*" element={<NotFound />} />
            </Routes>
        );
    };
    

    Root/package.json - Edit Scripts only

    {
        "scripts": {
             "dev": "cross-env NODE_ENV=development node ./server.js",
             "build": "npm run build:client && npm run build:server",
             "build:client": "vite build --ssrManifest --outDir dist/client",
             "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
             "serve": "cross-env NODE_ENV=production node ./server.js"
        }
    }
    

    Root/index.html - Required to Edit

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite + React + TS</title>
      </head>
      <body>
        <div id="app">
            <!--app-html-->
        </div>
        <script type="module" src="/src/entry-client.tsx"></script>
      </body>
    </html>
    
    

    Root/src/pages/Home.tsx - Optional Normal React Component

    export const Home = () => {
        return <div>This is the Home Page</div>;
    };
    

    Root/src/pages/Other.tsx - Optional Normal React Component

    export const Other= () => {
        return <div>This is the Another Page</div>;
    };
    

    Root/src/pages/NotFound.tsx - Optional Normal React Component

    export const NotFound = () => {
        return <div>Not Found</div>;
    };
    
    yarn dev
    

    Outputs

    Homepage

    Not Found OR 404 ERROR