Engineering

A tale of React Server Side Rendering

I want to start by mentioning that as of starting this I didn’t have a lot of experience with SSR besides adding styled components in a NextJS app.

What’s the story here?

So I have this website I created called https://awesometalks.party that was being rendered on the client. I was having issues with SEO and I also wanted to be cool of course and render it on the server for performance and coolness bonus.

I did this using Razzle that is this really awesome tool and this meant that I had a server.js that ran on the server, an app.js that was isomorphic and also a client.js for all your client needs.

Here is an example of the most basic server you can use with Razzle: https://github.com/jaredpalmer/razzle/blob/next/examples/basic/src/server.js

Before you call me a noob, that I am, I want to say that this project was not just a React project but it also included several things that needed to be SSR’d as well, like:

So we are going to start with the ones that went super smoothly:

React Router (AKA: At least Michael Jackson loves me)

React Router is the best, we all know that but what I didn’t know is that server side rendering it didn’t involve a single tear and it was by far the fastest one to do so. All you need is to spread the magic over three files.

So in your app that is isomorphic you place all the routes but without the actual react-router provider like so:

import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Home from './Pages/Home'
import Speakers from './Pages/Speakers'
import Speaker from './Pages/Speaker'
import Tags from './Pages/Tags'
import Tag from './Pages/Tag'
import Favorites from './Pages/Favorites'

export default () => (
    <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/favorites" component={Favorites} />
        <Route exact path="/categories" component={Tags} />
        <Route exact path="/category/:category" component={Tag} />
        <Route exact path="/speakers" component={Speakers} />
        <Route exact path="/speaker/:speaker" component={Speaker} />
    </Switch>
)

In the actual client.js you place the provider since this will only run on the client but the routes are for everything:

import React from 'react'
import { hydrate } from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom
import 'isomorphic-fetch'
import App from './App'

hydrate(
  <Router>
      <App />
  </Router>,
  document.getElementById('root')
)

if (module.hot) {
    module.hot.accept()
}

Why isomorphic-fetch? You never know in these things!

What about the server? Well you just have to wrap your component to be SSR’d in a Static Router and that is about the only change you need to do:

import { StaticRouter } from 'react-router'

...
const context = {}
const Root = () => (
    <StaticRouter location={req.url} context={context}>
        <App />
    </StaticRouter>
)

const markup = renderToString(<Root />);

That’s it! Those are the only differences we have to make in the server.js to get React Router working on the server as well 😀

Font Awesome (Yes, it needs SSR otherwise weird stuff happens)

I actually didn’t do this one, it was a PR but I have no idea where there are docs for this, but ok.

Font Awesome was implemented in a way where only the icons we needed were imported and added to the library so that everyone doesn’t get a 3mb icon font.

There is a icons.js with:

import fontawesome from '@fortawesome/fontawesome'
import faTimes from '@fortawesome/fontawesome-free-solid/faTimes'
import faSearch from '@fortawesome/fontawesome-free-solid/faSearch'

fontawesome.config = {
    autoAddCss: false
}

fontawesome.library.add(faSearch, faTimes)

Notice the config where we tell font awesome that we will add the CSS and instead of it being added by the fontawesome package automatically. After that, this file needs to be imported into our app.js, but on this side, that is it. You just need to know that this config exists.

Then on the server some markup had to be modified for us to add the actual CSS:

...
import fontawesome from '@fortawesome/fontawesome'

....
res.send(`<!doctype html>
    <html lang="en" ${helmet.htmlAttributes.toString()}>
    <head>
        ${assets.client.css
        ? `<link rel="stylesheet" href="${assets.client.css}">`
        : '' }
        ${process.env.NODE_ENV === 'production'
        ? `<script src="${assets.client.js}" defer>`
        : ``}
        ${fontawesome.dom.css()}
    
    
        ${markup}
    
`
)

By adding <style>$fontawesome.dom.css()</style> we now have perfect icons on both the client and the server and we can carry on.

Here is a link to actual PR.

React Helmet (GOOD DOCS ARE IMPORTANT)

Their docs are actually very good so you can see them here but what you need to change on the server is:

...
import { Helmet } from 'react-helmet'

....
const helmet = Helmet.renderStatic()
res.send(`<!doctype html>
    <html lang="en" ${helmet.htmlAttributes.toString()}>
    <head>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
        ${helmet.link.toString()}
        ${assets.client.css
        ? `<link rel="stylesheet" href="${assets.client.css}">`
        : '' }
        ${process.env.NODE_ENV === 'production'
        ? `<script src="${assets.client.js}" defer>`
        : ``}
        ${fontawesome.dom.css()}
    
    
        ${markup}
    
`
)

There are no changes needed on the client and your SEO will be on fleek from now!

Styled Components (css-in-js team)

These docs are also pretty amazing so let’s start by the changes in the server. First thing we need to do is wrap our root component in their theme provider because I am also using themes with styled components, so our root components will look like:

const Root = () => (
  <ThemeProvider theme={theme}>
      <StaticRouter location={req.url} context={context}>
          <App />
      </StaticRouter>
  </ThemeProvider>
)

After this provider component has been added to our root, we can follow the docs that tell us to create a new server stylesheet, collect the styles of the Root and get the style tags so this means we need to add this code:

import { ServerStyleSheet, ThemeProvider } from 'styled-components'

const sheet = new ServerStyleSheet()
// When the app is rendered collect the styles that are used inside it
const markup = renderToString(sheet.collectStyles(<Root />))
// Generate all the style tags so they can be rendered into the page
const styleTags = sheet.getStyleTags()

And now add these tags into the response we send to the client:

res.send(`<!doctype html>
    <html lang="en" ${helmet.htmlAttributes.toString()}>
    <head>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
        ${helmet.link.toString()}
        ${assets.client.css
        ? `<link rel="stylesheet" href="${assets.client.css}">`
        : '' }
        ${process.env.NODE_ENV === 'production'
        ? `<script src="${assets.client.js}" defer>`
        : ``}
        
        ${styleTags}
        ${fontawesome.dom.css()}
    
    
        ${markup}
    
`
)

Now a word of warning: I also had some globally injected styles for the body and general clean up of the page and these were being ignored by the SSR.

This was happening because I was importing them in the client.js instead of the app.js and this meant it only ran in the client.

If you have any type of global styles make sure to import them in the isomorphic part of the app like so:

import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Home from './Pages/Home'
import Speakers from './Pages/Speakers'
import Speaker from './Pages/Speaker'
import Tags from './Pages/Tags'
import Tag from './Pages/Tag'
import Favorites from './Pages/Favorites'
import './Utils/global-styles'
import './Utils/icons'

export default () => (
    <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/favorites" component={Favorites} />
        <Route exact path="/categories" component={Tags} />
        <Route exact path="/category/:category" component={Tag} />
        <Route exact path="/speakers" component={Speakers} />
        <Route exact path="/speaker/:speaker" component={Speaker} />
    </Switch>
)

And you are all set for styled components and now Apollo !

Apollo

I love Apollo, I do, but this was a pain also because I started with Apollo Boost just to make the config easier but then hit a wall and had to download all the packages.

Let’s start by getting the place where we create the Apollo Client away from the main initial files and put it into a file that in my case looked something like:

import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'

export default new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser,
    link: new HttpLink({
      uri: 'https://api.graphcms.com/simple/v1/cjhdcwrb98if90109o4pzawaq'
    }),
    cache: process.browser
            ? new InMemoryCache().restore(window.__APOLLO_STATE__)
            : new InMemoryCache()
})

And now you are thinking: What is this process.browser ? It’s just a way of figuring out if we are on the client or the server and render accordingly.

So we will initiate the ApolloClient with no devTools and in SSR Mode and then those will be switched.

And now comes this cache thing. So the way Apollo handles SSR is a little like styled components does where it gets all the things you are fetching in the page and dumps it in the dom so what this restore function does is look for the variable window.__APOLLO_STATE__ and restore it as the cache for the client as well. But we haven’t gotten this to work on the server so for now nothing will happen.

On the server there is a bunch of stuff we need to do, first thing is wrap our Root in the Apollo Provider:

import { getDataFromTree, ApolloProvider } from 'react-apollo'
import client from './Utils/apollo'
import theme from './Utils/theme'
...
const Root = () => (
    <ApolloProvider client={client}>
        <ThemeProvider theme={theme}>
            <StaticRouter location={req.url} context={context}>
                <App />
            </StaticRouter>
        </ThemeProvider>
    </ApolloProvider>

After this we need to use getDataFromTree and what this function does is get all the queries you have in your page and fetches them all and returns a promise, after this promise is resolved we can actually get the initialState and place it in the response like so:

server.get('/*', async (req, res) => {
  const Root = () => (
      <ApolloProvider client={client}>
          <ThemeProvider theme={theme}>
              <StaticRouter location={req.url} context={context}>
                  <App />
              </StaticRouter>
          </ThemeProvider>
      </ApolloProvider>
  )
  await getDataFromTree(<Root />)
  const initialApolloState = client.extract()
  
  ....
    res.send(`<!doctype html>
      <html lang="en" ${helmet.htmlAttributes.toString()}>
      <head>
          ${helmet.title.toString()}
          ${helmet.meta.toString()}
          ${helmet.link.toString()}
          ${assets.client.css
          ? `<link rel="stylesheet" href="${assets.client.css}">`
          : '' }
          ${process.env.NODE_ENV === 'production'
          ? `<script src="${assets.client.js}" defer>`
          : ``}
          
          ${styleTags}
          ${fontawesome.dom.css()}
      
      
          ${markup}
          
          window.__APOLLO_STATE__ = ${
            JSON.stringify(initialApolloState)
            .replace(/</g, '\\u003c')
          }
        
      
  `
  )
}

And this is where Apollo gets the initial state we saw in the in the above code and like this Apollo will render everything like magic.

This is what I learned in the lengthy discovery to get the App running on SSR using Razzle and I hope this is useful to someone going through the same struggles into SSR and React Apps.

For anything more here is the Github repo with all the code: https://github.com/SaraVieira/awesome-talks/tree/master

4 Tips to Improve Web Accessibility
was originally published in YLD Blog on Medium.
Share this article: