DEV Community

Cover image for A Comprehensive Guide to Building SSR Apps with React, React Router and Vite
Francisco Mendes
Francisco Mendes

Posted on • Edited on

A Comprehensive Guide to Building SSR Apps with React, React Router and Vite

Introduction

In recent years, there have been two immensely popular ways of rendering web pages, Single Page Applications and Server Side Rendering.

There are several tools and boilerplates that help us setup a React project to create SPA's, such as the famous create-react-app and vite. But when we talk about SSR, we are usually talking about frameworks, such as Next.js, Remix and Razzle.

However, although there are a lot of articles and tutorials on how to migrate an existing React application to Next.js, there is not much content on how to convert the current project from React to SSR without using a framework.

In this tutorial we will explore together how we can convert a React SPA using Vite to SSR.

What are we going to use?

In this tutorial, we are going to use the following technologies to create an SSR application:

  • React - react is a tool for building UI components
  • React Router - helps to manage the navigation among pages of various components in a react application
  • Vite - build tool that leverages the availability of ES Modules in the browser and compile-to-native bundler
  • h3 - a minimalistic and simple node.js framework
  • sirv - simple and easy middleware for serving static files
  • listhen - an elegant http listener

Prerequisites

Before going further, you need:

  • Node
  • Yarn
  • TypeScript
  • React

In addition, you are expected to have basic knowledge of these technologies.

Scaffolding the Vite Project

As a first step, create a project directory and navigate into it:

yarn create vite react-ssr --template react-ts
cd react-ssr
Enter fullscreen mode Exit fullscreen mode

Next, let's install the react router:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

Now we can create our pages inside src/pages/:

// @/src/pages/Home.tsx
export const Home = () => {
  return <div>This is the Home Page</div>;
};
Enter fullscreen mode Exit fullscreen mode
// @/src/pages/Other.tsx
export const Home = () => {
  return <div>This is the Other Page</div>;
};
Enter fullscreen mode Exit fullscreen mode
// @/src/pages/NotFound.tsx
export const NotFound = () => {
  return <div>Not Found</div>;
};
Enter fullscreen mode Exit fullscreen mode

Then we are going to rename our App.tsx to router.tsx and as you may have already guessed, it is in this file that we will define each of the routes of our application:

// @/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>
  );
};
Enter fullscreen mode Exit fullscreen mode

With our application pages created and the routes defined, we can now start working on our entry files.

Currently the only entry file we have in our project is main.tsx which we will rename to entry-client.tsx and this file will be responsible for being the entry point of the browser bundle and will make the page hydration.

// @/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>
);
Enter fullscreen mode Exit fullscreen mode

The next entry file that we are going to create is the entry-server.tsx in which we are going to export a function called render() that will receive a location (path) in the arguments, then render the page that was requested and end renders to a string (to be later added to the index.html on the node server).

// @/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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Last but not least, we need to make changes to index.html to look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite SSR + React + TS</title>
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
    <script type="module" src="/src/entry-client.tsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

With the client side of our application created, we can move on to the next step.

Create the Node Server

Before we start writing code, we need to install the necessary dependencies:

yarn add h3 sirv listhen
Enter fullscreen mode Exit fullscreen mode

The node server will be responsible for serving our application in the development and production environment. But these two environments are totally different and each one has its requirements.

The idea is that during the development environment we will use vite in the whole process, that is, it will be used as a dev server, it will transform the html and render the page.

While in the production environment what we want is to serve the static files that will be in the dist/client/ folder, as well as the JavaScript that we are going to run to render the pages will be in dist/server/ and that will be the one we are going to use. Here is an example:

// @/server.js
import fs from "fs";
import path from "path";

import { createApp } from "h3";
import { createServer as createViteServer } from "vite";
import { listen } from "listhen";
import sirv from "sirv";

const DEV_ENV = "development";

const bootstrap = async () => {
  const app = createApp();
  let vite;

  if (process.env.NODE_ENV === DEV_ENV) {
    vite = await createViteServer({
      server: { middlewareMode: true },
      appType: "custom",
    });

    app.use(vite.middlewares);
  } else {
    app.use(sirv("dist/client", {
        gzip: true,
      })
    );
  }

  app.use("*", async (req, res, next) => {
    const url = req.originalUrl;
    let template, render;

    try {
      if (process.env.NODE_ENV === DEV_ENV) {
        template = fs.readFileSync(path.resolve("./index.html"), "utf-8");

        template = await vite.transformIndexHtml(url, template);

        render = (await vite.ssrLoadModule("/src/entry-server.tsx")).render;
      } else {
        template = fs.readFileSync(
          path.resolve("dist/client/index.html"),
          "utf-8"
        );
        render = (await import("./dist/server/entry-server.js")).render;
      }

      const appHtml = await render({ path: url });

      const html = template.replace(`<!--ssr-outlet-->`, appHtml);

      res.statusCode = 200;
      res.setHeader("Content-Type", "text/html").end(html);
    } catch (error) {
      vite.ssrFixStacktrace(error);
      next(error);
    }
  });

  return { app };
};

bootstrap()
  .then(async ({ app }) => {
    await listen(app, { port: 3333 });
  })
  .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

With the node server explanation done and the example given, we can now add the following scripts to package.json:

{
  "dev": "NODE_ENV=development node server",
  "build": "yarn build:client && yarn build:server",
  "build:client": "vite build --outDir dist/client",
  "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
  "serve": "NODE_ENV=production node server"
}
Enter fullscreen mode Exit fullscreen mode

These are scripts that allows you to get the app up and running. If you want to start the development environment just run yarn dev, if you want to build the app just use yarn build, while yarn serve is to run the production environment.

If you go to http://localhost:3333 you should have the web application running.

Conclusion

As always, I hope you found the article interesting and that it helped you switch an existing application from React with Vite to SSR in an easier and more convenient way.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.

Have a nice day!

Top comments (15)

Collapse
 
jlguenego profile image
Jean-Louis GUENEGO

Weldone for this example. It save me time. 🙂

Just a small improvement: Test if the process.env.NODE_ENV is "production" because by default it is development as state the official node guys:
nodejs.dev/en/learn/nodejs-the-dif...

Collapse
 
rumbis profile image
George Alexandrou

Thank you very much

Collapse
 
redbar0n profile image
Magne

SSR with bare-bones Vite, such as with their react-ssr template, is pretty low-level / hardcore.

For an easier time, try vite-plugin-ssr.com/

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for the comment 😊, I happen to know this plugin and I really like it because it already has several standardizations such as data fetching 🚀.

However, I also think it's good to understand how things work behind the scenes since the tools we use on a daily basis have layers and layers of abstractions 🎁.

Collapse
 
redbar0n profile image
Magne

or even RakkasJS!

Collapse
 
andev70 profile image
Andev70

there is a mistake.

if you run following in the console
yarn create vite react-ssr --template react-ts
this will create only a client side react app template

to create a react-ssr(react-server-side-rendering app) with vite then you need to run following command in terminal

if you are using npm:

npm create vite@latest react-ssr

if you are using yarn :

yarn create vite react-ssr
## if you are using pnpm :
pnpm create vite@latest react-ssr

after running this command then a list of options will be shown in the terminal like following

❯ Vanilla
Vue
React
Preact
Lit
Svelte
Others

keep pressing the down arrow untill the arrow shown in the terminal options comes next to the other option and then press enter

after above action again a list will appear like following:

create-vite-extra ↗
create-electron-vite ↗
Enter fullscreen mode Exit fullscreen mode

choose the create vite extra option then enter

after that a list will appear again like following

❯   ssr-vanilla
    ssr-vue
    ssr-react
    ssr-preact
    ssr-svelte
    ssr-solid
    deno-vanilla
    deno-vue
    deno-react
    deno-preact
    deno-lit
    deno-svelte
    deno-solid
    library
    ssr-transform
Enter fullscreen mode Exit fullscreen mode

keep coming down untill you reach the react ssr then press enter then it will initialise a react-ssr project

then enter following commands, replace pnpm with whatever package manager you are using

cd react-ssr

pnpm i
Or 
yarn 
or 
npm install
Enter fullscreen mode Exit fullscreen mode

then you are done now you can run pnpm run dev to spin up the dev server

Collapse
 
dbish6 profile image
David Bishop • Edited

I don't understand how you would deploy an ssr application like this. When you're serving you are doing "NODE_ENV=production node server", which makes no sense because wouldn't you run the server from the build for production? This is my first time doing ssr and It is confusing me to the max how I can run the server in production, I have no clue. Please, someone, help me understand because you deploy the build folder, right. And that has no way to run the server when building with vite.

Collapse
 
sundayjava profile image
Sunday David Udoekong

I`m having issues here. Anytime i import deps using import ArrowBackIcon from '@mui/icons-material/ArrowBack'; it will not work. but if i use import {ArrowBack} from '@mui/icons-material'; it will work. and is not all deps they are named export, some are default export like react-slick. How can i solve this issue?

Collapse
 
sstark97 profile image
Aitor Santana Cabrera

Hello, I have a problem when I use these config in Netlify. I try to transform y CSR portfolio yo SSR and when I deploy t o Netlify doesnt work. Here my repository:
GitHub Repo

Collapse
 
jlguenego profile image
Jean-Louis GUENEGO

Note : When a web client request the url "/" the SIRV take the lead to give back index.html, so no SSR happens in this case.

Collapse
 
eduhsoto profile image
Eduardo Hernández Soto

why show when nmp run serve window = document.defaultView,
^

ReferenceError: document is not defined

Collapse
 
andev70 profile image
Andev70

I think you are using
in the wrong file, you should use the BrowseRouter component inside client-entry file

Collapse
 
kayodeadechinan profile image
Kayode Adechinan

Great article! Thansk for sharing. Please, how one can handle path variables ?