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 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.
- 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 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/