Vue + Relay + Server Side Rendering (SSR)
Tags: vue, relay, graphql, ssr
Published: 2020-08-04 02:43:12

_In which I describe how to achieve Server Side Rendering with vue-relay_

[GraphQL](https://graphql.org/) is a pretty swish alternative to [REST](https://restfulapi.net/).  That's not to say it's better or worse, but in certain situations it's pretty handy. [Relay](https://relay.dev/) is a JavaScript framework designed for hooking up [React](https://reactjs.org/) 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](https://apollo.vuejs.org/). 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](https://www.howtographql.com/react-apollo/0-introduction/) 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](https://github.com/vue-relay/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](https://relay.dev/docs/en/graphql-server-specification).  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](https://ssr.vuejs.org/).

If you don't have a server then I've written a post on how to do it with AWS Lambda@Edge.
{% link divporter/vue-serverless-side-rendering-with-aws-lambda-edge-1ep8 %}

#### **QueryRenderer**
I also assume that you know how to use [vue-relay](https://github.com/vue-relay/vue-relay).  If you don't, first  give the docs a read.  Also read and re-read the [Relay docs](https://relay.dev/docs/en/introduction-to-relay). 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](https://relay.dev/docs/en/graphql-in-relay.html#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](https://www.prisma.io/) have developed a [tool](https://github.com/prisma-labs/get-graphql-schema) 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](https://relay.dev/docs/en/network-layer) to setting up the Environment, then I'm going to make a few tweaks. We're going to define the Relay Environment within [Vuex](https://vuex.vuejs.org/) store.
{% gist https://gist.github.com/divporter/8b8137878f54609d0368193fe55e845c file=store.js %}

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.
{% gist https://gist.github.com/divporter/8b8137878f54609d0368193fe55e845c file=entry-server.js %}

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.