# How to build React SSR Application with Vite?
- **meta-title**: *React SSR Application with vite*
- **meta-description**: *Learning how to build a React Server side rendering app using vite, routing powered by React Router and APIs powered by Strapi.*
- **Figma images**:


- **Publish date**:
- **Reviewers**:
## Introduction
While **single-page applications (SPA)** are great, they lack support for good page speeds and SEO, which are critical to businesses and bring more revenue.
With **server-side rendering (SSR)**, we can build applications with good SEO and speed.
In this tutorial, we'll learn to build a meta-framework, like Next.js, with the following Features: A meta-framework is a development tool or framework built out of another framework. Examples include Next.js from React.js, Nuxt.js from Vue.js, and so on.
* Setting up a server to render the HTML from React.
* Support meta tags.
* Routing Logic.
* Render dynamic content on the server side with Strapi.
## Prerequisites
Understanding this blog requires some basic knowledge of the following topics:
* [NodeJS](https://nodejs.org) installed on system.
* Good Understanding of [ReactJS](https://react.dev), **Javascript** and [Express.js](https://expressjs.com).
* Knowledge about Bundlers, specifically about [Vite](https://vitejs.dev).
* Basic understanding of [Strapi](https://strapi.io).
* Server-side rendering and Client-s rendering.
* TypeScript: This blog is entirely based on Typescript-supported React, so please make sure you get an introduction to Typescript [here](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html). It's not necessary, but it's good to have.
## Why Vite?
[Vite](https://vitejs.dev/) is a popular build tool to convert applications like React to browser understandable code. Vite comes with great plugin system and great community to add more to it many popular frameworks. Qwik and Remix for example, are using Vite as their build tool to create production code.
## React SSR
When we create a React app either from biolerplates or from scratch we should be installing two dependencies i.e [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom). `react-dom` is the responsible one for converting React code into HTML code either on the server or client.
`react-dom` prodivdes a function `renderToString` from `react-dom/server` which we'll be using to convert the React to HTML.
## Setup
If you alreay have a Vite powered React app, you don't need to read this section, please forward to Next Section.
To create a React App powered by Vite, run the following command in your terminal or CMD and follow the questions promptly to set up the Vite-powered App. Please make sure to choose the `React` when asked for `framework` and choose `Typescript`.
```bash
npm create vite@latest
```
We're using `react-router` to implement routing, make sure you install `react-router-dom` into dependencies to run the application.
```shell!
npm i react-router-dom
```
### Creating Routes
As for the demo purpose, we're going to have a simple routing logic which is going to have 2 main routes & 1 one nested route.
We will be going to use [Data API Supported Routers](https://reactrouter.com/en/main/routers/picking-a-router#using-v64-data-apis)
> Data API routers useful for which the pages have heavy use of data, either loading the data or data mutations. These routers support loading data into routes or handle the mutations within component itself. To learn more click [here](https://github.com/remix-run/react-router/discussions/10204)
Here I'm picking `createBrowserRouter` and will be creating all my routes in a different file i.e I'm calling is `routes.jsx`.
> Defining Routes in a different place is useful because we have to re-use the same routes in multiple places i.e we have to create two routers one for server and one for client, defining routes in different place allows us to share the routes while using the different routers.
```javascript!
// location: src/routes.jsx
import axios from "axios";
import { IndexRouteObject, NonIndexRouteObject, json } from "react-router-dom";
import { Home } from "./Home";
type MetaAttributes = "name" | "http-equiv" | "charset" | "itemprop";
type Meta = { title: string } | Partial<Record<MetaAttributes, string>>;
export type MetaRouteObject =
| (IndexRouteObject & { meta?: React.ReactNode | Meta[] })
| (NonIndexRouteObject & {
meta?: React.ReactNode | Meta[];
children?: MetaRouteObject[];
});
const strapiService = axios.create({ baseURL: "http://127.0.0.1:1337/api" });
export const routes: MetaRouteObject[] = [
{
path: "/",
element: <Home />,
loader: async () => {
const foodList = (
await strapiService.get<StrapiResponse<Food[]>>("/foods")
).data;
return json(foodList.data ?? []);
},
meta: (
<>
<title>Home</title>
<meta name="title" content="Vite Server | Home" />
</>
),
children: [
{
path: "home",
element: (
<p>
Nested Screen, rendered on <kbd>/home</kbd>
</p>
),
meta: (
<>
<title>Nested Home</title>
<meta name="title" content="Vite Server" />
</>
),
},
],
},
{
path: "*",
element: <p>404, wrong Landing</p>,
meta: [{ title: "Hello" }, { name: "Hello" }],
},
];
```
The above code let's us create routes for the `react-router` while extending support for the use of `meta` tag supporting individual routes dynamically.
`meta` property can be any a React component i.e JSX or attributes supported by `meta` tag. Providing support of `meta` tags let's us improve the SEO of our webpage giving dynamic preview in social media platforms like `X` or `WhatsApp` etc.
`MetaRouteObject` is an extension of Type Provided by `react-router`'s `RouteObject` to support typings for the new `meta` property we've added.
As we're using the `Data APIs supported routes`, we should be executing the loaders at server-side, for that we need to write a function which will execute the `loader` data and push it into the `Router`'s context
To keep things simple, we're going to create the function in a different file i.e `fetch.ts`
```javascript!
// location: src/fetch.ts
import type * as express from "express";
export function createFetchRequest(
req: express.Request,
res: express.Response
): Request {
const origin = `${req.protocol}://${req.get("host")}`;
// Note: This had to take originalUrl into account for presumably vite's proxying
const url = new URL(req.originalUrl || req.url, origin);
const controller = new AbortController();
res.on("close", () => controller.abort());
const headers = new Headers();
for (const [key, values] of Object.entries(req.headers)) {
if (values) {
if (Array.isArray(values)) {
for (const value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
const init: RequestInit = {
method: req.method,
headers,
signal: controller.signal,
};
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = req.body;
}
return new Request(url.href, init);
}
```
The above function is used to transform `express` (we're using to convert react to html and serve html) and transform it to `Request` object which we can use
This `fetch` function will be used by `React Router` to execute the `loader` data and push into the Router's Context.
### Decoupling Server and Client.
What we have created using the above command is a Client Side app which now we need to decouple it to server & client.
`Vite` uses `main.tsx` or `main.jsx` as entry file, which we need to replace with two files `main-client.tsx` and `main-server.tsx`.
> File names can be anything, but just ensure we can simply differentiate between client & server easily.
### Client Code
```javascript
// name: main-client.tsx
import React from "react";
import { hydrateRoot } from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import "./index.css";
import { routes } from "./routes.tsx";
// creating Client-side Router
const router = createBrowserRouter(routes);
// Hydrating the component at client-side
hydrateRoot(
document.getElementById("root")!,
<React.StrictMode>
// Client-side Routers
<RouterProvider router={router} fallbackElement={null} />
</React.StrictMode>
);
```
### Server Code
```javascript
import React from "react";
import {
type StaticHandlerContext,
StaticRouterProvider,
createStaticRouter,
createStaticHandler,
} from "react-router-dom/server";
import { MetaRouteObject, routes } from "./routes.tsx";
import { createFetchRequest } from "./fetch.ts";
import type * as express from "express";
import { renderToString } from "react-dom/server";
import "./index.css";
interface RenderProps {
request: express.Request;
response: express.Response;
}
const handler = createStaticHandler(routes);
// Generate React-Component for the `meta` tags from `routes.jsx` file.
const getMeta = (matches: StaticHandlerContext["matches"]) => {
let matchedRoute = matches[matches.length - 1].route as MetaRouteObject;
// If there's any index route, we try to read the meta from it's parent route.
// If an index route is inside matched routes, just
if (matchedRoute.index) {
matchedRoute = matches[matches.length - 2].route as MetaRouteObject;
}
if (Array.isArray(matchedRoute.meta)) {
return (
<>
{matchedRoute.meta.map((meta, index) => {
return <meta key={index} {...meta} />;
})}
</>
);
}
return matchedRoute.meta;
};
// Function responsible to render server-side.
export async function render({ request, response }: RenderProps) {
// execute `loader` data for the matched route.
const remixRequest = createFetchRequest(request, response);
const context = await handler.query(remixRequest);
if (context instanceof Response) {
throw context;
}
// Server-Side Router
const router = createStaticRouter(
handler.dataRoutes,
context as StaticHandlerContext
);
const metaComponent = getMeta(context.matches);
// Render the meta component into server-side
const headContent = renderToString(metaComponent);
// Generate Main HTML
const html = renderToString(
<React.StrictMode>
<StaticRouterProvider router={router} context={context} />
</React.StrictMode>
);
return { html, head: headContent };
}
```
### Creating a Server
Now that we have all things required form the react side to generate the SSR app, we're missing the server which is responsible to serve the generated HTML. We'll be using the `express` we've installed before to serve the app using `vite`.
Create a file called `server.js` in the root folder of your app and copy the below contents to into the file.
```javascript!
// name: server.js
import express from "express";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "node:url";
import { createServer as createViteServer } from "vite";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const port = process.env.PORT || 5173;
const base = process.env.BASE || "/";
/**
* @type {string}
*/
let template;
/**
* @type {import("vite").ViteDevServer | null}
*/
let vite;
const isProduction = process.env.NODE_ENV === "production";
const app = express();
async function loadTemplate() {
if (isProduction) {
template = fs.readFileSync(
path.resolve(__dirname, "client/index.html"),
"utf-8"
);
const compression = (await import("compression")).default;
const sirv = (await import("sirv")).default;
app.use(compression());
app.use(base, sirv("./dist/client", { extensions: [] }));
} else {
vite = await createViteServer({
appType: "custom",
server: { middlewareMode: true },
});
app.use(vite.middlewares);
template = fs.readFileSync(path.resolve(__dirname, "index.html"), "utf-8");
}
return vite;
}
const startServer = async () => {
await loadTemplate();
app.use("*", async (req, res) => {
let render, templateHTML;
if (isProduction) {
render = (await import("./dist/server/entry-server.js")).render;
templateHTML = template;
} else {
templateHTML = await vite.transformIndexHtml(req.originalUrl, template);
render = (await vite.ssrLoadModule("./src/entry-server.tsx")).render;
}
const { html: appHtml, head } = await render({
request: req,
response: res,
});
// 5. Inject the app-rendered HTML into the template.
let html = templateHTML.replace(`<!--ssr-outlet-->`, appHtml);
html = html.replace(
"<!--meta-outlet-->",
head ?? "<title>React SSR App </title>"
);
// 6. Send the rendered HTML back.
res.status(200).set({ "Content-Type": "text/html" }).end(html);
});
app.listen(port);
console.log(`Checkout: http://localhost:${port}`);
};
startServer();
```
The above file contains the code related to serving the App in the both `development` and `production` mode. The main difference in between the both modes are just that how we load the files. In `development` mode we load the react code and transpile it on the go whereas the in `production` mode the code is already compiled to `javascript` and can be easily convereted into HTML without any step required from vite to `transpile` the code.
**Update `Package.json`**
We should be updating our package.json file to support new npm commands to be able to spin up a express server to build and server the html
```json
// Package.json content
"scripts":{
//"...existing scripts"
"dev": "node server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx",
"build": "npm run build:client && npm run build:server",
"serve":"NODE_ENV=production node server"
}
```
Now that we have all code setup for serving the SSR application, let's load the application with some API data using [Strapi](https://strapi.io/)
Please follow this [guide](https://docs.strapi.io/dev-docs/quick-start) to install strapi and run things quicker.
In our example, we're going to create a new Content-Type called `Food` which will going to have following properties.
* name
* category
* recipe
The aim is to load the food items from `strapi` on Server Side.
### Creating Food Content-type

As I have stated above, we should be creating a content-type with the properties `name`, `category` and `recipe` which we use to serve the API.
> Please read this [guide](https://docs.strapi.io/dev-docs/api/rest) to understand how to access the API's for the newly created content.
**Please make sure you create some sample data before hitting the API.**

### Consuming the API
To load the content on server-side we should be using `loader` property in the `routes.jsx`.
I'm going to consume the API for home route i.e `/` path.
> http://localhost:1337/api/foods is the API we should be hitting.
After doing the changes `routes.jsx` should look like below
```javascript!
// name: routes.jsx
//
// Make sure to install `axios`.
import axios from 'axios'
const strapiService = axios.create({ baseURL: "http://127.0.0.1:1337/api" });
const routes = [
{
path: "/",
element: <Home />,
loader: async () => {
const foodList = (
await strapiService.get<StrapiResponse<Food[]>>("/foods")
).data;
return json(foodList.data ?? []);
},
meta: (
<>
<title>Home</title>
<meta name="title" content="Vite Server | Home" />
</>
),
},
...children
// ....existing routes
]
```
We're using `<Home />` as `element` for path `/`, but we haven't created it anywhere, let's do that now.
#### `<Home />` component
```javascript!
import { Outlet, useLoaderData } from "react-router-dom";
export const Home = () => {
const foodList = useLoaderData();
if (!foodList) {
return <p>No Food items found</p>;
}
return (
<>
{(foodList as Food[]).map((food: Food) => {
return (
<div
key={food.id}
style={{
border: "1px solid red",
padding: "4px",
marginBottom: 5,
borderRadius: 5,
}}
>
<p>{food.attributes.name}</p>
<p>{food.attributes.category}</p>
<p>{food.attributes.recipe}</p>
</div>
);
})}
<Outlet />
</>
);
};
```
Here we're using `useLoaderData` function to read the data which was returned from `loader` function and then rendering based on the data.
This all while no API call happening at browser, all from server 🚀.
### Building & Serving
To build the application we need to build for `Server` and `Client` both of them. The commands to build for production is available as a npm script which we can use as follows.
```shell!
npm run build
```
To serve the application we need a server runtime i.e A system where we can run `Node.js` Server to run the production build of Application we should be having `NODE_ENV` variable value to `PRODUCTION`. To keep things simple we can simple run the npm script `preview` command i.e
```shell!
npm run serve
```
> For production we're using `compression` package which gives the support for `gzip` compression to reduce the data transfer and compression.
## Demo.

**You can access code for this blog [here](https://github.com/b4s36t4/Strapi-SSR-Demo)**
### References
[ViteJS](https://vitejs.dev)
[React Router](https://reactrouter.com)
[SSR Examples](https://github.com/bluwy/create-vite-extra/tree/master/template-deno-react-ts)
## Conclusion
While Meta frameworks like `Next.js` and `Remix` are very good, we should know how they're working under the hood and how we can implement them with whatever tools we have. Implementations may vary but the way we do with what tools we implement is how things work. Here we've used `Vite` and `Router Dom` which are very great tools what they're built for along with the things what advanced.
# Review
* Please give a detailed installation process of the project.
* The GIF you provided is not clear enough. Could you please share with me on Github since Hackmd supports an upload of 1MB.