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.
8 responses to “How to SSR with `apollostack` and `meteor-react-router-ssr`”
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.
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?
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.
commented on
On the server, you’d have access to the request headers right? Or am I missing something else here?
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.
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>
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.