A Comprehensive Guide to Building SSR Apps with React, React Router and Vite
Tags: javascript, react, node, tutorial
Published: 2022-08-11 12:35:00

### 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](https://github.com/facebook/react) - react is a tool for building UI components
- [React Router](https://github.com/remix-run/react-router) - helps to manage the navigation among pages of various components in a react application
- [Vite](https://github.com/vitejs/vite) - build tool that leverages the availability of ES Modules in the browser and compile-to-native bundler
- [h3](https://github.com/unjs/h3) - a minimalistic and simple node.js framework
- [sirv](https://github.com/lukeed/sirv) - simple and easy middleware for serving static files
- [listhen](https://github.com/unjs/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:

```sh
yarn create vite react-ssr --template react-ts
cd react-ssr
```

Next, let's install the react router:

```sh
yarn add react-router-dom
```

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

```js
// @/src/pages/Home.tsx
export const Home = () => {
  return <div>This is the Home Page</div>;
};
```

```js
// @/src/pages/Other.tsx
export const Home = () => {
  return <div>This is the Other Page</div>;
};
```

```js
// @/src/pages/NotFound.tsx
export const NotFound = () => {
  return <div>Not Found</div>;
};
```

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:

```js
// @/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>
  );
};
```

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.

```js
// @/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>
);
```

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).

```js
// @/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>
  );
};
```

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

```html
<!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>
```

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:

```sh
yarn add h3 sirv listhen
```

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:

```js
// @/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);
```

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

```js
{
  "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"
}
```

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](https://github.com/FranciscoMendes10866/react-vite-ssr) the link to the github repository.

Have a nice day!