TNS
VOXPOP
Do You Resent AI?
If you’re a developer, do you resent generative AI’s ability to write code?
Yes, because I spent a lot of time learning how to code.
0%
Yes, because I fear that employers will replace me and/or my peers with it.
0%
Yes, because too much investment is going to AI at the expense of other needs.
0%
No, because it makes too many programming mistakes.
0%
No, because it can’t replace what I do.
0%
No, because it is a tool that will help me be more productive.
0%
No, I am a highly evolved being and resent nothing.
0%
I don’t think much about AI.
0%
Frontend Development

How to Build a Server-Side React App Using Vite and Express

A demo of server-side rendering and server-side data fetching without using a framework — showing what React-powered frameworks actually do.
Nov 6th, 2023 9:08am by
Featued image for: How to Build a Server-Side React App Using Vite and Express

In this post I’ll explain how you can enable server-side rendering and server-side data fetching in React… without using a framework!

Whilst the code in this post isn’t what I’d call “production ready”, it should help explain two of React’s built-in methods,hydrateRoot and renderToString, both of which are required to enable server-side rendering in React.

If you’re keen to jump ahead, all the code used in this post can be seen in the following GitHub repository.

Two small caveats:

  1. I won’t be covering how to deploy a React SSR Application.
  2. Most of what I’ll be explaining can be found in the Vite docs: Server-side Rendering.

Setup and Install Dependencies

The first thing you’ll need is to initialize a new npm package. (the -y flag skips the questionnaire and uses the npm defaults when creating a package.json)


Now you can install the dependencies.


And lastly, install the development dependencies.

Add Scripts to package.json

There are five scripts that you’ll need to add. One is for development, the remaining four are for creating a production build, plus a serve script so you can preview the production build in the browser.

  1. dev. This script starts the Vite development server.
  2. build:client. This script bundles the index.html and entry-client.jsx.
  3. build:server. This script bundles entry-server.jsx.
  4. build. This script runs both of the above “build:” scripts.
  5. serve: This script runs server-prod.js (I’ll explain what this is shortly)>

Add type:module to package.json

Vite’s dev server uses native ES Modules, so you’ll need to add “type” : “module” to your package.json. If you don’t do this you’ll likely see errors relating to: Cannot use import statement outside a module.

Creating the src Files

index.html

At the root of your project created a file called index.html. This acts as the “template” for the application. There are two things to note in this file.

  1. The div id of ”app” is the target DOM node used by React when hydrateRoot is called.
  2. The comment of <!–outlet–> is replaced by the server with the result of React’s renderToString function.

app.jsx

Create a src directory at the root of your project, then create a file called app.jsx.

This is a simple function component that returns some basic HTML to be rendered by the browser. The component uses export default syntax.

 entry-client.jsx

Create a file in the src directory called entry-client.jsx. This file is responsible for displaying the <App /> component in the div with an id of app.

You can read more about hydrateRoot in the React docs here: hydrateRoot.

entry-server.jsx

Create a file in the src directory called entry-server.jsx. This file is responsible for “converting” the <App /> component into a plain HTML string suitable for use in the browser.

You can read more about renderToString in the React docs here: renderToString. This file exports a named function called render.

Vite Config

At the root of your project create a file named vite.config.js and add the following code snippet.

Creating The Development Server

Create a file in the root of your project called server-dev.js. This is the server that is started when you run npm run dev.

app = express()

As you’d expect, this is an instance of Express, it’s common to define it as a const called app.

createServer

This creates a Vite development server, the additional config is required is so Vite knows to hand control over to express.

app.use(vite.middleware)

This ensures any requests to express get passed back over to the Vite development server.

app.use(‘*’)

The express app deals with all incoming requests, the url from each request can be extracted from the req object and is required by Vite when transforming index.html.

template

The template is the starting point for the page. It’s populated with the HTML from app.jsx on the server by the render function, and then re-populated again, or hydrated, in the browser using the same HTML from app.jsx.

{ render }

As mentioned above, this function is responsible for “converting” React code into a plain HTML string.

html

This is where everything comes together. Using .replace you can target the from index.html and replace it with the return value from the render function.

.end(html)

Using the standard .end() Express method you can return a status 200, set the content type, then pass in the HTML to display in the browser.

These are the fundamental principles behind rendering React on the server; but since Vite is a development tool, you’ll have to make some changes in order to create an Express server that would work if deployed.

Creating The Production Server

Create a file in the root of your project called server-prod.js. This is the server that is started when you run npm run serve. The production server is very similar to the development server, with some notable differences.

No Vite

Vite is only used while in development mode. When you run npm run build, Vite uses Rollup under the hood to compile all the required files and output them to a npm run build directory. As such, there’s no need to use Vite’s createServer in production.

express.static

The express.static() function is built-in middleware and can be used to serve the static files (HTML, .js) that are required for React to run in the browser.

template and { render }

These are broadly the same as in the development server, but the Vite specific methods (transformIndexHtml and ssrLoadModule) have been removed. The paths now point to the ./dist directory instead of the src files.

That wraps up the first part of this post, but there’s one piece missing…

Server-Side Data Fetching

A React app that has server-side rendering capabilities can also take advantage of server-side date fetching. There are two advantages to this.

  1. Server-side requests can be used to make secure connections with databases (for example).
  2. Data from the server-side request will still be displayed in the browser even when JavaScript is disabled, or before hydration has occurred.

There are quite a few changes required in order for this to work and I’ll explain what each of them are; but, if you’d prefer to see them on GitHub, I’ve prepared a pull request with all the changes on the following link.

package.json

Add a new script called “build:function” and point it to a new function.js file (which you’ll create next). Then modify the build script to include && npm run build:function.

function.js

Create a file in the src directory called function.js. This file contains an async function called getServerData that will be called from the server.

server-dev.js

The changes here relate to importing the new function.js file, calling the getServerData async function then passing the data back to the render function. It’s also necessary to create a script element that will populate window.__data__ with the newly fetched server data. Adding the data to the window will allow React to access the same data as the server when it hydrates the page.

server-prod.js

The changes here are almost identical to the changes made to the development server, with the exception of the path where function.js can be found.

entry-client.jsx

The changes here are to define a new variable called data, then set it to equal the value of window.__data__. With the data variable now containing the data that was requested server-side, it can be passed on to the <App /> component via a prop called data.

entry-server.jsx

It’s a similar story with entry-server.jsx; but this time, instead of grabbing the data from the window object, you can access the data that was passed through from the server when the render function was called with a parameter of data. The same approach can then be used to pass the data on to the <App /> component via a prop called data.

app.jsx

The last change to make is to de-structure the new data prop and return it in an HTML <pre> element so it’s visible on the page.

Finished

And there you have it, server-side rendering and server-side data fetching without using a framework!

I’ve been using React since ~2017 and I never really understood hydrateRoot or renderToString, but working through this example project I now have a much better understanding of how React actually works — and much more appreciation for what React-powered frameworks actually do.

I’m also really impressed with Vite — everything from the docs to the developer experience is top-notch. Plus, in case you missed it, Remix just announced their new Vite plugin; and if the Remix team are using Vite, that’s a good indication that Vite is every bit as good as it looks!

Group Created with Sketch.
TNS DAILY NEWSLETTER Receive a free roundup of the most recent TNS articles in your inbox each day.