Adding Server Side Rendering to a Relay Production App

Sibelius Seraphini
6 min readFeb 27, 2020
Photo by Dave Webb on Unsplash

Reality

Reality has a surprising amount of detail (https://johnsalvatier.org/blog/2017/reality-has-a-surprising-amount-of-detail). Reading all blog posts about Server Side Rendering (SSR) is not enough to properly implement it in a production app. The same is valid for most of development tasks, like setting up a react native project, fixing a webpack weird bug and so on. You need to get your hands dirty with "reality" to really understand what tiny details really matters.

Our Frontend Stack

A bit of context of this task, we work on Feedback House (https://feedback.house/), a platform to manage teams. One of our module is a hiring platform, where candidates can apply to job posting and manage their applications (https://entria.contrata.vc/).

We decided to move this module/frontend to use SSR to improve social media sharing, and improve head meta tags.

This frontend uses the following stack: react, styled-components, material-ui, styled-system, loadable-components, react-router and relay.

SSR "framework"

We checked pure webpack solution, razzle, nextjs, afterjs.

Nextjs won't work well to us, as we have nested routes, and managing persisted layout patterns would be a big change for us (https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/)

Afterjs was too high level, and pure webpack was too low level.

Razzle was in the right spot, razzle is just 2 webpack combined.

Basic Razzle knowledge

Commands

razzle start: start your development application (compiles both server and client)

razzle build: build your app to production usage

Files/Structure

razzle.config.js: let you bring plugins and modify webpack/babel and other configs

index.js/ts: server entrypoint - basic HMR

server.tsx: server itself (express/koa) that will render React app on server

client.tsx: client entrypoint that will hydrate SSR render

Facing Reality

We started following razzle examples and tutorials, and keep bumping on "issues" that I will describe it here.

Fixing Typescript

Razzle has a razzle.config.js config file that let you config any part of their config (webpack, babel and so on).

my razzle.config.js looks like this:

inside webRazzlePlugin with have a modify function to return a custom webpack config

To make webpack transpile typescript .ts and .tsx files with added new extensions like this:

We also had to remove `strictExportPresence` webpack config (https://webpack.js.org/configuration/module/#module-contexts), so import types won't cause compilations errors, just warnings

Fixing Monorepo

We modify babel loader to transpile all monorepo packages to let us modify any package and reload our main frontend:

Fixing .env

We let our environment variable inside .env files to make it easy to build on CI. We added dotenv-webpack to make this possible:

https://gist.github.com/sibelius/a85b79e0720ab6795fe01e9ef7a2c3ec

First SSR Render

The first SSR render was just a loading component in a html file \o/

Just calling renderToString is not enough to properly render a complex app

Fixing React-Router

We use a static route config (https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config). And we use nested route to handle persistent layout, routed tabs, routed modal and more.

You need to use StaticRouter on server and BrowserRouter on client to make them work well:

client:

StaticRouter will set context.url if there is some redirects while rendering

You gonna need Extractors

styled-components, material-ui and loadable-components, all have "extractors". They will collect styles and chunks to be added to your SSR render

Without this your first render won't work well, your components won't be styled correct, and code split won't work out of the box.

Checkpoint

After all this work, we expect to have at least a better first render experience. However, SSR is harder than it sounds. After all this, we still have a simple <Loading /> component \o/.

Fixing Relay (Data Fetching)

We need to fetch and store all GraphQL queries before rendering our components.

I've followed this 2 examples to make this possible (https://github.com/jaredpalmer/react-router-nextjs-like-data-fetching and https://github.com/relayjs/relay-examples/tree/master/issue-tracker)

IssueTracker example uses preloadQuery + usePreloadQuery that required react and relay experimental builds, and our production code still can't move to it, as we still need to fix some StrictMode issues. So we have a mixed approached.

The first "trick" is to colocate query and variables on each route, like this:

require __generated__ is the same as using graphql`` tag

queriesParams will get variables based on match params.

query let us fetch route data dependencies before rendering the component, this also let us prefetch code and data and follow render-as-you-fetch React pattern.

Prefetching Relay queries per route

We use matchRoutes from react-router-config to find which routes has matched, it can be more than one, as we have nested routes.

After that we fetch all queries using relay fetchQuery (https://relay.dev/docs/en/fetch-query):

Rendering with Relay store data

All queries made using fetchQuery will store data on a Relay Environment, and we gonna used it on our RelayEnvironmentProvider

The trick here is to always use the correct fetchPolicy store-and-network (https://relay.dev/docs/en/query-renderer#props) on QueryRenderer, so it will reuse Environment data, instead of sending another request.

Make RelayEnvironment work on both client and server

When on server, we create a new Relay Environment per request, so we don't leak user data to another user. On client we reuse the Environment and start the store using some records if availables.

We store all Relay Store records inside window.__RELAY_PAYLOADS__, so we hydrate Relay store on client and avoid request the same data again.

On client with create Relay Store like this:

After use of __RELAY_PAYLOADS__ we remove it from window.

Where we are?

After all this work, we have a nice first render without loading.

However this is not enough, as we have some private routes that needs also to be rendered properly.

Fixing authentication (localstorage)

Most client side apps using localstorage to manage authentication as session storage looks like a bunch of work, and cookies looks like an outdated and "complex" solution.

However when you want to SSR authed routes you can't rely on localstorage, as it does not work on server.

The first thing to do, it to make your GraphQL server set authentication cookies httpOnly after a login/signup process:

After this you need to modify your Relay Network Layer (https://medium.com/entria/relay-modern-network-deep-dive-ec187629dfd3) fetch call, to use credentials: ‘include’. This will send cookies to your server automatically (magic), but it will break if your frontend and server is on different domains (dammit CORS).

You can fix CORS, using a proxy on your SSR server to "fake" that your GraphQL server is in the same domain as your frontend. On production you can use a nginx to fix this.

ExecuteEnvironment

ExecuteEnvironment will help you check if you are running code on server or client:

This is same/similar to Relay ExecuteEnvironment codebase.

Fixing hostname

To fix some isomorphic problems like checking what is the hostname, I've come up with a global.ssr that contains some helpers:

After that we can have a isomorphic getDomainName like this:

After Thoughts

Is that all folks? I don't think so, there are still some issues that needs to be solved

to improve this SSR approach.

  • use @defer to avoid fetching to much data on server
  • check new React streaming api

This write has not all the details, ping me on twitter to discuss more about it (https://twitter.com/sseraphini)

You can also learn more about relay using my open sourced course (https://github.com/sibelius/relay-modern-course)

You can watch me demo some cool Relay features to React Europe here https://twitter.com/ReactEurope/status/1226951417002446849, you can play with the demo here https://react-europe-relay-workshop.now.sh/

If you wanna more hands on on Relay, check React Europe Relay Workshop (https://twitter.com/reacteurope/status/1194908997452795904?s=21), I'll show all this and more advanced Relay patterns.

dev.to version: https://dev.to/sibelius/adding-server-side-rendering-to-a-relay-production-app-30oc

Newsletter

Subscribe to my newsletter for new content https://sibelius.substack.com/

--

--