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

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.