Server-Side Rendering with React and TypeScript

Galen Weber
Atticus Engineering
7 min readMar 28, 2018
React + Server + TypeScript

Looking to implement server-side rendering with React and Typescript? We were but we couldn’t find a good guide — so after we developed our approach, we wrote one. Our focus was on simplicity, performance, and limiting outside dependencies.

Why Server-side Rendering, React, and TypeScript

At Atticus, we build applications for people with serious legal needs. Our core product analyzes a user’s situation to offer them free, trusted advice and connect them with the right lawyer or service to solve their problem. It’s a high-stakes offering with little room for error, so our tools have to be reliable, fast, and easy-to-use.

With React as our UI library, we knew that we’d have great page-to-page load times, but a bad initial load time.

With React as our UI library, we knew that we’d have great page-to-page load times, but a bad initial load time, since React cannot render until the full .js bundle has reached the client. To solve that issue, we turned to server-side rendering (SSR). SSR lets us serve fully rendered HTML on the first HTTP request (before the client .js bundle has loaded), cutting our “Time to First Meaningful Paint” significantly. Adding TypeScript to the mix was a no-brainer: in our experience it sharply reduces the number of errors entering the build, reduces time spent on debugging, and accelerates the development cycle.

Rolling our own

There are growing number of frameworks and libraries for server-side React. We’re particularly excited about Airnbnb’s Hypernova service. Ultimately, however, we decided to roll our own, for two reasons:

  1. No existing service has the community support and flexibility we’d want for such an integral part of our stack.
  2. There’s a simple path to implementing SSR using tools already a part of the standard build process (i.e. Webpack).

In our experience, scaffolding a React application to support SSR and TypeScript requires a moderate, one-off time expenditure. But that is more than outweighed by the time savings for our developers (via TypeScript) and for our users (via faster loads).

We’ve outlined our “vanilla” SSR implementation below. Our goal is not to advocate for particular technologies, but to describe the generic challenges to server-side rendering React, and present one approach that worked well for us.

In our experience, scaffolding a React application to support SSR and TypeScript requires a moderate, one-off time expenditure. But that is more than outweighed by the time savings for our developers (via TypeScript) and for our users (via faster loads).

1. SSR from 30,000 feet

At the highest level, a server-side-rendered React application works as follows:

  1. Client makes an HTTP request to the server
  2. Server renders the page’s React components to a string
  3. Server injects the string into HTML
  4. Server sends the fully rendered HTML page to the client
  5. Once the page loads, client side JS takes over

There are several steps to a React SSR app. To start with, here’s the directory structure that will serve as the scaffold for our app:

sample-ssr-proj/
dist/
js/
css/
media/
node_modules/
src/
components/
containers/
server/
theme/
types/
package.json
tsconfig.json
webpack.config.js

Like almost any React project, we have a src/ directory and a dist/ directory. Since our source code includes JSX, which browsers can’t interpret, we need to run it through a transpiler. We use these two directories to demarcate between our source code (in src/) and the code “distributed” to browsers (in dist/). And because we’re already transpiling our code (converting JSX to vanilla JS), it’s relatively trivial to incorporate TypeScript: all it requires is another transpilation step in the build process.

Now, our directory structure contains a server/folder, which is not seen in the average React project (such as one generated with Create React App). In a simple React app, the server is “dumb”: all it does is spit out one “empty” HTML page that includes a <script /> link to the React bundle, which contains all the necessary logic to render the view.

We want our server to evaluate our JSX components, render a React view to a string, and embed that in the HTML sent to the client. That means our server needs to be “smart” (or at least relatively so), and that our server code will need to be transpiled. And since our server will use Node’s networking libraries, it must be transpiled distinctly from the client JS bundle, which will use browser-specific libraries.

We want our server to evaluate our JSX components, render a React view to a string, and embed that in the HTML sent to the client. That means our server needs to be “smart”.

1. Define a Simple Component

Start with a simple React component that can be rendered on the client and on the server. The component will be a counter, where clicking a button increments a number displayed to the user. Name component Counter and place it in the containers/ directory. This will be the only page (and only component) in our app.

React “counter” component

2. Render on the Client

Rendering this on the client side should be a familiar process for any React programmer. We add another file (clientEntry.tsx), import the component, and pass it into ReactDOM.render(), along with a reference to a DOM element to render the component to.

Code to render a React component into an HTML “container” element

We’ll place clientEntry.tsx in our src/ directory as a direct sibling to components/ and containers/.

We’ll also write a simple HTML page that includes the element with id “app” referenced above:

“Empty” HTML that loads React bundle

Note that the HTML file also includes a<script /> link to /js/client.js. That is the output bundle (located in dist/) that we will produce when we transpile our code. We’ll be using Webpack for that process, but will refrain from defining the build configuration until we’ve considered the server side process.

3. Render on the Server

On the server side, the steps are less conventional. Start by defining a simple Express server in src/server/index.ts.

Simple Express server

All the server above does is reply to every request with "hello world". We want it to respond with an HTML page that includes our React component.

To do that, we’re going define a function html that accepts a string of rendered React, and returns the same HTML we defined earlier, but with that string injected exactly where React would inject the component. We’ll do this using JavaScript’s template literals, and place the function in a file titled simply html.ts.

Function to inject a React string into an HTML string

So now to serve our React page from the server, we simply import the component, render it to a string using the renderToString method from the react-dom/server library, inject that into the HTML using the function above, and send that down to the client.

Our server code with those additions:

Server that renders React to string, injects into HTML, and sends to client

We’re now defining the HTML in html.ts so we can delete the index.html file we defined earlier (note that we still include the <script /> tag linking to the React bundle in our new HTML).

4. Building our bundles

We now have our server and client code, both of which make use of JSX and TypeScript. Neither our browser nor Node can interpret this code, so let’s define build processes to produce vanilla JS that can be executed. Mentally, we can divide our source code into three categories:

  1. Server-specific code (everything in server/ )
  2. Client-specific code ( clientEntry.tsx )
  3. React components (used by both server and client)

One Webpack build process will transpile our server (and the referenced JSX components) into vanilla Node. A second Webpack build process will follow the standard configuration used for a React SPA, outputting a client.js bundle that we fetch from our HTML. Once that’s received, React hooks into the already-rendered page, and any logic that we’ve defined becomes available.

As mentioned, we use Webpack to manage the transpilations. We’ll define our configurations in webpack.config.js:

Webpack allows us to define our configuration as a function (rather than a standard JSON object), and we leverage that here in order to define multiple configurations in a single file. When we run Webpack from the command line, we’ll set properties on the env argument to indicate which configuration object (that for the server or that for the client) should be returned.

// run the server-side webpack build
webpack --env.platform=server
// run the client-side webpack build
webpack --env.platform=web

The Webpack configuration we defined above is pretty basic. It transpiles all the TypeScript and JSX code in the dependency graph of the provided entry point (src/server/index.ts for the server build andsrc/clientEntry.tsx for the client) and writes the result as a bundle in our dist/ folder. We use ts-loader to perform the actual JSX and TypeScript transpilation and include a tsconfig.json file in the root for configuration (see that file here).

At this point, our build process can’t handle other file formats (like .css or .png). But this is all we need to serve a basic HTML page that’s rendered on the server and dynamic on the client side.

Running it all together

Executing the two Webpack CLI commands will create two files in our dist/js directory: client.js and server.js. Since this is a Node application, we start it up with:

node dist/js/server.js
// Example app listening on port 3000!

If we navigate to localhost:3000 we see our (incredible) app. The message and the button render on the first HTTP request, even before the client.js bundle has been downloaded. And once the bundle is downloaded, we have the full interactive capabilities of a standard React app.

There’s still more to be done (like handling routing and styling) to produce a full-fledged application. But with this base for a React TypeScript SSR application, those features are relatively straightforward to implement.

--

--