Table of contents
Before diving, if you don't know what a SSR application is let me give you a brief about it. SSR is the method of generating HTML at server side instead of in client browser (CSR).
With growing list of frameworks we just are building applications where we write code in Javascript and let the bundlers, browsers to handle the generation of HTML. Example we have React, with react we're doing CSR because the code you've written is rendered on client-side thus it's called CSR, if it does at server-side then it's SSR.
How react is CSR?, if you ever built a react-application you should've seen the following script in the index.html
file we started application
<noscript>You need to enable JavaScript to run this app.</noscript>
Well, if you understand what CSR and SSR is, let's dive into creating a Server Side application.
Checkout this article for more info on CSR vs SSR: https://www.freecodecamp.org/news/server-side-rendering-javascript/
How?
We're going to use vite
as the bundler as vite does have some default helpful functions that make SSR easier.
Before diving in, we should know two functions from react itself i.e renderToString
and hydrateRoot
.
renderToString
will take a component and create's HTML tree for us to paint on the browser -> renderToString
hydrateRoot
lets you display React components inside a browser DOM node whose HTML content was previously generated by -> hydrateRoot
Using vite
as our bundler gives us the ability spin up SSR easily.
Tech-stack
React, obviously
Vite - as a bundler
React router - Routing & data provider
Styled-component - Styling.
express - running server on
To Start a basic repo you can run the following command npx create vite-app
and follow the instructions given by CLI to create the app.
Once everything is done, you can use the default node package manager to install the run the commands to start server or build server.
// I am using pnpm as my package manager
pnpm install // install the packages
pnpm dev // start dev server
Creating a Server.
We're aiming for Server side rendering, so we obviously have to render it in server. we have to use renderToString
function provided from react-dom/server
which let's us to render a react component in server side without need of browser.
Let's create a file named entry-server.jsx
under /src
folder.
Doesn't have to be
entry-server.jsx
but sure to have.jsx
extension and remember name.
import React from "react";
import ReactDOMServer from "react-dom/server";
import {StaticRouter} from "react-router-dom/server";
import App from "./App";
import { ServerStyleSheet } from "styled-components";
// This syntax is important,
// which keeps the same export name even after bundling.
export function render(url) {
const sheet = new ServerStyleSheet();
const html = ReactDOMServer.renderToString(
sheet.collectStyles(
<React.StrictMode>
<StaticRouter url={url}>
<App />
</StaticRouter>
</React.StrictMode>
)
);
const styleTags = sheet.getStyleTags();
return { html, styles: styleTags };
}
Creating a Client.
The Rendering happened at Server side should be hydrated at client-side to make all the react code to work as we expected.
Don't be fear of hydration errors, leave them.
Let's create entry-client.jsx
inside /src
folder.
Same rules, your code your filename :)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
ReactDOM.hydrateRoot(
document.getElementById("app"),
<React.StrictMode>
<BrowserRouter>
<LazyMotion features={loadFeatures}>
<App />
</BrowserRouter>
</UserContextProvider>
</React.StrictMode>
);
hydrate
lets you display React components inside a browser DOM node whose HTML content was previously generated byreact-dom/server
.
We have the rest of react ready i.e <App />
component.
Now we need to serve the HTML over the server, so we need a server code to run the server.
server.js
in root of your folder.
import fs from "node:fs/promises";
import express from "express";
// Constants
const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5000;
const base = process.env.BASE || "/";
let handler;
// Cached production assets
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;
// Create http server
const app = express();
// Add Vite or respective production middlewares
let vite;
if (!isProduction) {
const { createServer } = await import("vite");
vite = await createServer({
server: { middlewareMode: true },
appType: "custom",
base,
});
app.use(vite.middlewares);
} else {
const compression = (await import("compression")).default;
const sirv = (await import("sirv")).default;
app.use(compression());
app.use(
base,
sirv("./dist/client", {
extensions: [],
// We're compressing to brotli
brotli: true,
})
);
}
// Serve HTML
app.use("*", async (req, res) => {
try {
const url = req.originalUrl.replace(base, "");
let template;
let render;
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile("./index.html", "utf-8");
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule("/src/entry-server.jsx")).render;
} else {
// Code that is compiled.
template = templateHtml;
render = (await import("./dist/server/entry-server.js")).render;
}
// Here we need req.originalUrl as `location` param in `StaticRouter` param is expecting path with a forward-slash(/)
const rendered = await render(
req.originalUrl,
);
const html = template
.replace(`<!--app-head-->`, rendered.head ?? "")
.replace(`<!--app-html-->`, rendered.html ?? "")
// Created for our own purpose
// We inject custom styles/script values from server to reduce latency
.replace(`/* app-script */`, rendered.script ?? "")
.replace("<!--app-style-->", rendered.styles ?? "");
res.status(200).set({ "Content-Type": "text/html" }).end(html);
} catch (e) {
vite?.ssrFixStacktrace(e);
console.log(e.stack);
res.status(500).end(e.stack);
}
});
// Start http server
app.listen(port, async () => {
console.log(`Server started at http://localhost:${port}`);
});
Now that we have server.js
to run the server and serve our react based HTML code we need to prepare out html
in which our generated react will be appended to.
If we observe the code inside server.js
we're appending few special strings to index.html
before sending it over server as string.
The below block of code.
const html = template
.replace(`<!--app-head-->`, rendered.head ?? "")
.replace(`<!--app-html-->`, rendered.html ?? "")
// Created for our own purpose
// We inject custom styles/script values from server to reduce latency
.replace(`/* app-script */`, rendered.script ?? "")
.replace("<!--app-style-->", rendered.styles ?? "");
<html>
<head>
<title>Sunfox Solar Cost and Savings Calculator</title>
<!--app-style--> Custom style injected at rendering time.
<!--app-head--> Custom head component injected at rendering time.
</head>
<div id="app"><!--app-html--></div> Html content replaced by rendered code.
<script type="module" src="/src/entry-client.jsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
/* app-script */ Any custom JS code to add to HTML, might be useful in case of using redux.
</script>
</html>
Now that we have everything we needed, let me put a simple description about everyfile so that we don't have to go back&forth to make changes.
entry-server.jsx
to Render the react at server-side, along withRouter
.entry-client.jsx
to Update the rendered react at browser side i.ehydrating
server.js
to run the server which serves the generatedreact
code.index.html
the template HTML which will append the generated react and serve over network.
Updating scripts
inside package.json
, since the base project was setup using vite
we will be having this below template scripts
section.
{
"scripts": {
"dev": "pnpm dev",
"build": "pnpm build"
}
}
Let's do the following changes into to package.json
{
"scripts": {
"dev": "node server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --ssrManifest --outDir dist/client --minify esbuild",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server --minify esbuild",
"preview": "cross-env NODE_ENV=production node server.js"
}
}
Let me explain the each command indivudally.
dev
to run the dev server.build
to build the code for production.preview
start the server on production code.
This guide is more of a setup SSR with vite
and React-router
, if you're just checking/learning about vite SSR here's a good start for you. https://vitejs.dev/guide/ssr.html#server-side-rendering
https://reactrouter.com/en/main/guides/ssr
If you want more of clean experience of building SSR application with vite without the hassle, vike is the best place to start. https://vike.dev.
This content only covers the StaticRouter
part of react-router
, since v6
we also have an option to use loaders
with router from react-router
. if anybody want an implementation of that version too, please comment :).
Tips
Wrap a route in
Suspense
, if you want to render the component in client-side.fallback
prop withSuspense
is bit tricky part, so be wise before using it :). https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-contentIf a part of your content is only need to render at client, utilize the
useEffect
// Example implementation of a hook to determine browser/server.
import { useEffect, useState } from "react";
export const useIsClient = () => {
const [isClient, setIsClient] = useState();
useEffect(() => {
if (typeof window !== "undefined" && window.document; || !import.meta.env.SSR) {
setIsClient(true);
} else {
setIsClient(false);
}
}, []);
return { isClient };
};
References.
https://vitejs.dev/guide/ssr.html#server-side-rendering
https://reactrouter.com/en/main/guides/ssr
Thanks for your time ❤️.