Futuristic illustration of a cloud server setup with glowing cables and floating monitors.

How to SSR with `apollostack` and `meteor-react-router-ssr`

Filed in ,

💡

Originally written in 2016, when Meteor was still a solid choice for full-stack JavaScript. Preserved as is, with updated links where needed.

Wiring up meteor with Apollo and adding a router was easy enough, but getting server-side rendering and populating the client with an initial state was quite challenging.

The documentation at dev.apollodata.com is fairly extensive but ridiculous to navigate. The sidebar is filled with menu items that only appear as you scroll – making it impossible to locate that piece of information that you were sure you read somewhere. It would really help if they had an option to search too.

Captain Jean-Luc Picard from Star Trek performing a facepalm, expressing frustration
When the docs sidebar expands only after you scroll—and that one crucial link vanishes into oblivion.

After piecing in bits and bytes from all over the place,  I am writing this in the hope that it might make your life a little easier if you were after the same thing. First, I had to hack meteor-react-router-ssr and move preRender down so it runs after the wrapperHook. I also added app as an argument. (See below)

Here’s how my isomorphic index.jsx looks after that:

import React from 'react';
import { Meteor } from 'meteor/meteor';
/*
* Had to hack server.jsx in meteor-react-router-ssr to move the  
* `preRender` hook so it runs after `wrapperHook` and I had to 
* give it {app} as an extra  argument.
*/
import { ReactRouterSSR } from 'meteor/jasonnathan:react-router-ssr';
import ReactHelmet from 'react-helmet';
import AppRoutes from '/imports/routes.jsx';
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { getDataFromTree } from "react-apollo/server";
let client, initialState,
  url = "localhost",
  opts = {
    ssrMode: Meteor.isServer
  };
// relevant for apollo when a client-side query is made
if (Meteor.isClient && process.env.NODE_ENV === 'production') {
  url = "public-url.com";
}
// setup apollo's networkInterface
opts.networkInterface = createNetworkInterface({
  credentials: 'same-origin', 
  uri: `http://${url}:3000/graphql`
});
const rehydrateHook = state => initialState = state;
const wrapperHook = app => {
  opts.initialState = initialState;
  client = new ApolloClient(opts);
  return { app };
};
// the preRender is simpler. All it needed was the `app` argument
const preRender = (req, res, app) => 
  Promise.await(getDataFromTree(app));
// dehydrating in a way that is apollo friendly. 
// Queries & mutations need to be removed
const dehydrateHook = () => ({
  apollo: { 
    data: client.store.getState().apollo.data 
  }
});
const htmlHook = html => {
  const h = ReactHelmet.rewind();
  return html.replace(
    '', '' + h.title + h.base + h.meta + h.link + h.script
  );
};
// the weirdest thing - wrapperHook in clientOptions - 
// inferring it only runs on the client
const clientOptions = { wrapperHook, rehydrateHook };
const serverOptions = { htmlHook, preRender, dehydrateHook };
ReactRouterSSR.Run(AppRoutes(), clientOptions, serverOptions);
import React from 'react';
import { Meteor } from 'meteor/meteor';
/*
* Had to hack server.jsx in meteor-react-router-ssr to move the  
* `preRender` hook so it runs after `wrapperHook` and I had to 
* give it {app} as an extra  argument.
*/
import { ReactRouterSSR } from 'meteor/jasonnathan:react-router-ssr';
import ReactHelmet from 'react-helmet';
import AppRoutes from '/imports/routes.jsx';
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { getDataFromTree } from "react-apollo/server";
let client, initialState,
  url = "localhost",
  opts = {
    ssrMode: Meteor.isServer
  };
// relevant for apollo when a client-side query is made
if (Meteor.isClient && process.env.NODE_ENV === 'production') {
  url = "public-url.com";
}
// setup apollo's networkInterface
opts.networkInterface = createNetworkInterface({
  credentials: 'same-origin', 
  uri: `http://${url}:3000/graphql`
});
const rehydrateHook = state => initialState = state;
const wrapperHook = app => {
  opts.initialState = initialState;
  client = new ApolloClient(opts);
  return { app };
};
// the preRender is simpler. All it needed was the `app` argument
const preRender = (req, res, app) => 
  Promise.await(getDataFromTree(app));
// dehydrating in a way that is apollo friendly. 
// Queries & mutations need to be removed
const dehydrateHook = () => ({
  apollo: { 
    data: client.store.getState().apollo.data 
  }
});
const htmlHook = html => {
  const h = ReactHelmet.rewind();
  return html.replace(
    '', '' + h.title + h.base + h.meta + h.link + h.script
  );
};
// the weirdest thing - wrapperHook in clientOptions - 
// inferring it only runs on the client
const clientOptions = { wrapperHook, rehydrateHook };
const serverOptions = { htmlHook, preRender, dehydrateHook };
ReactRouterSSR.Run(AppRoutes(), clientOptions, serverOptions);

And here’s the modified portion of react-router-ssr:

// from line 190      
// if (serverOptions.preRender) {
//   serverOptions.preRender(req, res);
// }
//...
global.__STYLE_COLLECTOR_MODULES__ = [];
global.__STYLE_COLLECTOR__ = '';
renderProps = {
  ...renderProps,
  ...serverOptions.props
};
fetchComponentData(serverOptions, renderProps);
  let app;
  if (typeof clientOptions.wrapperHook === 'function') {
     app = clientOptions.wrapperHook(app);
  }
    // preRender moved after wrapperHook
  if (serverOptions.preRender) {
    serverOptions.preRender(req, res, app);
  }

That’s it! You can grab a copy of the modified meteor-react-router-ssr from here

💡

Originally written in 2016, when Meteor was still a solid choice for full-stack JavaScript. Preserved as is, with updated links where needed.

8 responses to “How to SSR with `apollostack` and `meteor-react-router-ssr`”

  • anson Avatar
    anson

    commented on

    I found you recently posted a article that is related to react-router-ssr with apollo ssr.
    I wonder if you could upload a sample project to github to show how to do it? It is because my existing project is also using react-router-ssr, apollo, redux, but I have not idea how to hack into it.

    And I believe many people would love to know how to do that.

    Many thanks.

    •  Avatar
      GuruGeek

      commented on

      Sure but this article is about using it with Meteor, hence `meteor-react-router-ssr`. If you’re using that then the modified version of the repo is at pipewrk/meteor-react-router-ssr.

      You can find an isomorphic `index.jsx` I used to wire everything together in this website’s main repo here pipewrk/www.jasonnathan.com.

      Is this what you’re looking for? Are you only using react-router? If so, which version?

  • Jackson Avatar
    Jackson

    commented on

    Thanks for the great article Jason, this works perfectly!

    Do you have any thoughts on how you might access the hostname in a isomorphic setup?

    My app looks up the current user by subdomain which is then used in the apollo queries, but I cant find an isomorphic way to do this. i.e something like window.location.hostname in the components obviously wont work when rendered on the server.

    •  Avatar
      GuruGeek

      commented on

      On the server, you’d have access to the request headers right? Or am I missing something else here?

  • Jackson Avatar
    Jackson

    commented on

    No, I’m missing something!

    So I can access the req headers on the server, but then what happens when the client takes over after the initial render? I had a non meteor version working but I had to use Redux to set the store on the server and then used that in my quries.

  • Jackson Avatar
    Jackson

    commented on

    As far as I can tell, you cant set something like user: { username: 'johndoe' } directly on ApolloClient without an apollo query.

    My Redux version looked something like this on the server:

    const currentUser = getUserByHostname(req.headers.host)
    const initialState = { currentUser }
    const store = configureStore(initialState, client)

    <apolloprovider store="{store}" client="{client}">
      <routercontext {...renderprops}=""/>
    </apolloprovider>

    •  Avatar
      GuruGeek

      commented on

      I’ve read that it is possible to instantiate a store per route (and it some places, it is recommended) Perhaps you can wrap your provider around each route rather than something global since this will be valid in your use case.