loading...
Cover image for Vue + Relay + Server Side Rendering (SSR)

Vue + Relay + Server Side Rendering (SSR)

divporter profile image David Porter ・6 min read

In which I describe how to achieve Server Side Rendering with vue-relay

GraphQL is a pretty swish alternative to REST. That's not to say it's better or worse, but in certain situations it's pretty handy. Relay is a JavaScript framework designed for hooking up React applications with a GraphQL API. GraphQL + React + Relay are the foundations of the current Facebook application. All three are open-sourced, developed by Facebook and designed for Facebook. This means that whilst they are strongly supported and maintained they are primarily geared towards Facebook's needs and concerns.

Why not use Apollo?

Well, my recommendation would be to use Apollo. It's flexible, easy to get started and most pertinent to this article, it's SSR ready. There is a nice article at How To GraphQL which explains the difference between Apollo, React and urql.

In summary Relay is very rigid and demands strict protocols to be followed. This is handy for Facebook so that amongst all their developers Relay is used consistently across the board. For developers starting out it can be a steep learning curve and for small applications is probably not worth the hassle. It also requires a strong coupling between the API and the client. That's not such a bad thing, but quite a departure from REST.

Pushing on with vue-relay

So for whatever reason, we're sticking with Relay and we're going to use vue-relay.

Assumptions

Architecture

Although a front-end framework Relay demands that your GraphQL server implements a few things. You can read about these here. In short you must:

  1. Implement the Node interface
  2. Implement a refetch mechanism based on the global ID
  3. Implement a connection model for slicing and paginating through queries that return lists

Note that you don't need to do these from scratch. Whichever language (Node.js, Python, Go, etc) you have used to implement your GraphQL server with, it's highly likely that implementations exist for supporting relay.

Server Side Rendering (SSR)

This guide assumes that you've got SSR in place, but you can't seem to hook it up with relay. If you don't already have SSR implemented then checkout out the Vue SSR Guide.

If you don't have a server then I've written a post on how to do it with AWS Lambda@Edge.

QueryRenderer

I also assume that you know how to use vue-relay. If you don't, first give the docs a read. Also read and re-read the Relay docs. Relay is so rigid that every last detail counts.

GraphQL schema

Not SSR specific, but worth a mention. When working with relay you need to compile your queries first with relay-compiler. Another reason why I would recommend Apollo, but it does help you fix up any mistakes in your schema and queries and if you're like me you make loads.

In order to run the compiler you need the API schema in a schema.graphl file accessible from your client side code. If your schema is defined in code, then you'll need to export the compiled schema. As an alternative the smart cookies at Prisma have developed a tool to introspect your GraphQL API and spit out the schema for you. Very convenient.

Prefetching

A good starting place is fetching the data. I'm going to use serverPrefetch which is available in components for Vue versions 2.6.0+. I recommend you do too, because implementing it yourself is cumbersome, but not impossible.

relay-runtime provides a fetchQuery method for manually querying your GraphQL API. fetchQuery requires the same things that the QueryRenderer requires. An environment, a query and some variables. So it would look something like this.

import { fetchQuery } from 'vue-relay'
...
serverPrefetch(){
  return fetchQuery(this.environment, this.rootQuery, this.variables)
}

Environment

If you've followed the Relay Guide to setting up the Environment, then I'm going to make a few tweaks. We're going to define the Relay Environment within Vuex store.

Stepping through the important parts we've set up the initial state like so

state: () => ({
    environment: null,
    cache: new QueryResponseCache({ size: 250, ttl: 60*1000 })
})

We start with environment set to null and setup the cache using the QueryResponseCache constructor provided by relay-runtime. It will become clear why soon.

Next we define an init action. This will setup the environment using the cache. We will use Environment from relay-runtime which looks like this.

const environment = new Environment({
  network,
  store
})

for the network part we supply a network layer using Network.create() again from relay-runtime. The things to note here are that instead of defining the cache in the global scope we reference the cache that we defined in the Vuex state.

const cache = state.cache;

The cache is important because we are going to populate it with the result from our GraphQL API. Then when hydration occurs on the client the QueryRenderer will retrieve the results from the cache.

Next, you'll notice that unlike the Relay documentation I have opted to use axios over fetch. This is because axios works on both the client and the server. You could use fetch if you used something like node-fetch on the server.

Then at the end we update the environment in our state

commit("setItem", {key: "environment", value: environment})

QueryRenderer

Now we need to update the props we pass to the QueryRenderer. Here is what it should look like in your template block.

<query-renderer :environment="environment" :query="rootQuery"
                    :variables="variables" v-slot="{ props, error}">

The environment props needs to come from Vuex store so we add a computed property

computed: {
  environment(){ return this.$store.state.environment }
}

entry-server.js

Here is our entry point to our app for SSR.

Of note here is

await store.dispatch("init")

Here we are calling the init action we defined just above to set up our Relay Environment.

context.rendered = () => {
  const responses = {}
  store.state.cache._responses.forEach( (value, key) => {
    responses[key] = value
  })
  store.state.cache._responses = responses
  context.state = store.state
};

Another reason to go for Apollo here. The QueryResponseCache that powers our cache is a Map. When we render the page we also need to pass the state of the application through. The state is simply serialized with JSON.stringify. But you can't serialize a Map. Fortunately for the cache key we use the query and variables as a string. So we can convert this Map to an Object, update the store.state and set the context.state.

entry-client.js

Finally we're going to tie it all together in the hydration step. On the entry point of our application, if we're hydrating a rendered page we need to update our Vuex store to match. Normally this is straight-forward but recall that we just butchered the cache in order to serialize it. Now we need to convert it back to a Map.

If there is a window.__INITIAL_STATE__ we do the following to convert the _responses Object back to a Map.

const responses = new Map()
// We initialize the store state with the data injected from the server
if (window.__INITIAL_STATE__.cache && window.__INITIAL_STATE__.cache._responses){
  for (const [key, value] of Object.entries( window.__INITIAL_STATE__.cache._responses)) {
      responses.set(key, value)
  }
}

Then we override the Vuex state, initialize the cache and update the cache._responses with the Map we just created.

store.replaceState(window.__INITIAL_STATE__)
store.commit("setItem", {key: "cache", value: new QueryResponseCache({ size: 250, ttl: 60*1000 }) })
store.commit("setCacheResponses", responses)

Then finally call the init action to set up the Relay Environment.

As this is a simplified application, if yours is more complex you need to ensure that the init action doesn't undo the work of SSR by resetting anything that was set with store.replaceState. You can do this by specifying this in a separate action and dispatching on the client only for example.

Summary

Unless you're working at Facebook, use Apollo.

Posted on by:

divporter profile

David Porter

@divporter

Serverless and SPA/PWA enthusiast

Discussion

pic
Editor guide
 

Just to clarify, is all this SSR stuff only so that web crawlers can access your site? I don't really see why else SSR would be useful nowadays.

 

It can also be useful to improve time to first time paint: Downloading a non interactive version of your first page (plain html) is usually quite cheap. Downloading+parsing+executing a big fat js can take some time, and people like the "wow" effect of a website that loads instantly.

 

For this I would recommend pre-rendering if you can get away with it. This is typically done with a headless browser module like puppeteer. It's generally easier to implement but too slow to do on the fly so it's done offline.

Right, but pre-rendering is hard when you have user data (ex: a public personal profile). But anyway, is there much difference between pre-rendering and SSR with a cache ?
For instance, when running your SSR on lambda@edge, cloudfront can cache the results, which makes further requests kind-of pre-rendered, isnt it ?

By the way, to mention my comment on your other article: I precisely used Cloudflare Workers to do just that (and for their <10ms cold startup time). They make it really easy to tweak caching via their distributed key-value store which you can access from your edge locations => That's way more powerful compared to a cloudfront cache you cannot control. I'll post an article about that when I have time to spare.

I use pre-rendering to render the shell of the app (nav, footer, background etc) for a rapid first paint. It's much easier to set up. But you're spot on, with caching the actual SSR won't happen that often. I only do full SSR for SEO, regular users get the pre-rendered shell.

 

Yes, in a similar vein if you implement meta tags for Facebook in your Vue app, then without SSR the Facebook crawler will only see index.html. I don't imagine SSR will be required in future, but for now it is.

 

Nice conclusion :)