DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Sibelius Seraphini
Sibelius Seraphini

Posted on

Adding Server Side Rendering to a Relay Production App

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 the 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 understand what tiny details that matter.


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 modules is a hiring platform, where candidates can apply to a 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 a nested route, 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 entry point - basic HMRΒ 
server.tsx: server itself (express/koa) that will render React app on server
client.tsx: client entry point that will hydrate SSR render


Facing Reality

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

Fixing TypescriptΒ 
Razzle has a razzle.config.js config file that lets 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:

First SSRΒ Render

The first SSR render was just a loading component in an 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 the 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 component \o/.

Fixing Relay (Data Fetching)
We need to fetch and store all GraphQL queries before rendering our components.
I've followed these 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 approach.
The first "tricky" 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 lets us fetch route data dependencies before rendering the component, this also lets us prefetch code and data and follow render-as-you-fetch Reat pattern.

Prefetching Relay queries per route
We use matchRoutes from react-router-config to find which routes have 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 going to use 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 the server, we create a new Relay Environment per request, so we don't leak user data to another user. On the client we reuse the Environment and start the store using some records if available.
We store all Relay Store records inside window.RELAY_PAYLOADS, so we hydrate Relay store on the client and avoid request the same data again.

On the client with create Relay Store like this:

After the use of RELAY_PAYLOADS we remove it from the 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 look 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 the 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 nginx to fix this.

ExecuteEnvironment

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

This is the 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 an isomorphic getDomainName like this:


After Thoughts

Is that all folks? I don't think so, there are still some issues that need to be solvedΒ to improve this SSR approach.
decide to render a mobile or a desktop version on SSR
fixing Head tags and react-helmet (https://twitter.com/sseraphini/status/1232726960494780416), it looks like this is not so easy in React \o/
useIsClient hook to defer from rendering only to client-side

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


This write has not all the details, ping me on twitter to discuss more 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 to React Europe Relay Workshop (https://twitter.com/reacteurope/status/1194908997452795904?s=21), I'll show all this and more advanced Relay patterns.

medium version: https://medium.com/@sibelius/adding-server-side-rendering-to-a-relay-production-app-8df64495aebf?postPublishedType=repub

Top comments (0)

🌚 Life is too short to browse without dark mode