To set up server-side rendering (SSR) with React, Vite for the frontend, and Express for the backend using modern JavaScript, follow these simplified steps:
1. Set Up Your Project
First, create a new directory and initialize your project:
mkdir react-vite-ssr
cd react-vite-ssr
npm init -y
2. Install Dependencies
Install the necessary packages for Vite, React, Express, and some additional dependencies:
npm install react react-dom express vite
npm install --save-dev @vitejs/plugin-react
3. Configure Vite
Create a vite.config.js
file in the root of your project to set up Vite for React:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: './src/entry-client.jsx',
},
},
});
4. Set Up React Entry Files
Create two entry files, one for the client and one for the server.
src/entry-client.jsx
:
This file is for client-side rendering:
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('app'), <App />);
src/entry-server.jsx
:
This file is for server-side rendering:
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
export function render() {
return renderToString(<App />);
}
5. Create a React Component
Create a simple React component in src/App.jsx
:
import React from 'react';
export default function App() {
return (
<div>
<h1>Hello from React with SSR!</h1>
</div>
);
}
6. Create index.html
Create an index.html file in the root of your project
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React SSR</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/entry-client.jsx"></script>
</body>
</html>
7. Set Up Express Server
Create an server.js
file for your Express server in the root of your project:
import express from 'express';
import { createServer as createViteServer } from 'vite';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function createServer() {
const app = express();
// Create Vite server in middleware mode
const vite = await createViteServer({
server: { middlewareMode: 'ssr' },
});
// Use Vite's connect instance as middleware
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
import { createServer as createViteServer } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
async function createServer() {
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'
})
// Use vite's connect instance as middleware. If you use your own
// express router (express.Router()), you should use router.use
// When the server restarts (for example after the user modifies
// vite.config.js), `vite.middlewares` is still going to be the same
// reference (with a new internal stack of Vite and plugin-injected
// middlewares). The following is valid even after restarts.
app.use(vite.middlewares)
app.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
// 1. Read index.html
let template = fs.readFileSync(
path.resolve(__dirname, '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. ssrLoadModule automatically transforms
// ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
const { render } = await vite.ssrLoadModule('/src/entry-server.jsx')
// 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(url)
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
// 6. Send the rendered HTML back.
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// 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)
}
})
app.listen(3000)
}
createServer()
7. Edit package.json
{
"name": "react-vite-ssr",
"type": "module",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "node server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.19.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vite": "^5.4.3"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1"
}
}
8. Run the Server
To run your server, execute the following command:
npm run dev
This will start the Express server, with Vite handling the frontend during development. Your application will be available at http://localhost:3000
.
Summary
This setup allows you to develop a React app with SSR using Vite for the frontend and Express for the backend. The configuration is simple, and you can easily extend it to add more features as needed. This approach takes advantage of modern JavaScript features and provides a smooth development experience with Vite.