SSR with server-render: A potential strategy


#1

This is an announce + help kind of a post.

Till now I had been using ssrwpo:ssr@3.0.0 with awesome results. But it uses electrode-react-ssr-caching which is not working with Reactv16 yet.

So I started looking at the server-render package and it too is great. Kudos to @benjamn.
https://blog.meteor.com/meteor-platform-is-still-alive-5f6426644555 helped me understand the process, and then I started tweaking.

This is what I am doing now:

  1. On the server I have defined the following:
Meteor.subscribe = function() {
	return {
		ready() {return true;}
	};
};
  1. With hack 1, I am able to run my withTrackers on server too. withTracker is run once and only once on the server, hence I can easily set state in it so I use the following:
const CompWithTracker = = withTracker(() => {
  // subscribe and get data

  if (Meteor.isServer) actionToUpdateStore({type, data});
  return data; // this ensures that on the client, the data from state is overwritten by that from meteor.
});
// mapDispatchToProps contains actionToUpdateStore, and mapStateToProps contains the needed states
const CompWithStore = connect(mapStateToProps, mapDispatchToProps)(CompWithTracker);
  1. Now while rendering, I do double rendering, the first one to set the states, and the second to generate html.
renderToString(<div id='react'><App location={sink.request.url} /></div>); // to set store
const preloadedState = store.getState();

sink.appendToBody(`
    <script>
      window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
    </script>
`);
sink.appendToBody(renderToString(<div id='react'><App location={sink.request.url} /></div>));

The approach works fine, the only visible problem being a lot of server cpu usage.

Pros:

  1. I don’t have to recreate the whole data methods on the server. Complete de-duplication.
  2. No extra parsing and checking routes. React Router v4 does everything already.

Cons:

  1. No caching.
  2. Poor server perf.

Questions:

  1. What are the potential pitfalls of this strategy, apart from server performance?
  2. How can I measure and improve server performance?

#2

This is an interesting approach. The first thing that comes to mind is how this will be used in a “view”, say in a React element. Here you are loading the preloaded state, but I’m curious how you intend to re-hydrate it since the withTracker code cannot access the actionToUpdateStore method on the client?


#3

actionToUpdateStore comes from the mapDispatchToProps, both on the server and the client. connect is the general way to access it anywhere, and that’s what I am using.

On server, withTracker function is run once (as per the react-meteor-data package) and hence I update the store on the server (only), while on the client I overwrite the received prop from the store (through connect), by the data received through publish. So the rehydration is through a direct prop (which overwrites store’s value).

We can also update the redux store on the client if we want, but it may cause unintended rerenders.

eg.

const UnitPageWithTracker = withTracker(({match, setUnitPageData}) => {
	let unitId = match.params.unitId;
	if (!unitId) return {};

	const unit = Units.findOne({_id: unitId}, {fields: {_id: 1}});
	if (!unit) return {};
	if (Meteor.isServer) setUnitPageData({type: 'UNIT__SET_UNIT', unit});
	return {unit};
})(UnitPage);

const {setUnitPageData} = require('/imports/redux/actions');
const mapStateToProps = ({unitPageData}) => ({unit: unitPageData.unit});
const mapDispatchToProps = (dispatch) => {
	return {
		setUnitPageData(opt) {
			dispatch(setUnitPageData(opt));
		},
	};
};

const UnitPageWithStore = connect(mapStateToProps, mapDispatchToProps)(UnitPageWithTracker);

By the way, the server performance is not that poor. I have been running a testing version and it seems to be working fine with server usage largely similar to that of original ssrwpo:ssr. It seems React 16 does some optimizations of its own. So far, the methodology seems to be working great.

Moreover, going a bit off topic, I have also used dynamic-import along the lines of More about Dynamic Imports, A Few Questions on the client side, like this

async function main() {
	const [
		{Meteor},
		{Accounts},
		React,
		{render},
		{combineReducers, createStore, applyMiddleware},
		{Provider},
		{default: thunk},
		{BrowserRouter},
		{onPageLoad},
		{default: MainApp},
		appReducers,
	] = await Promise.all([
		import('meteor/meteor'),
		import('meteor/accounts-base'),
		import('react'),
		import('react-dom'),
		import('redux'),
		import('react-redux'),
		import('redux-thunk'),
		import('react-router-dom'),
		import('meteor/server-render'),
		import('/imports/app'),
		import('/imports/redux/reducers'),
	]);

	const preloadedState = window.__PRELOADED_STATE__;
	delete window.__PRELOADED_STATE__;

	const store = createStore(combineReducers(appReducers), preloadedState, applyMiddleware(thunk));

	const App = () => (
		<Provider store={store}>
			<BrowserRouter>
				<MainApp/>
			</BrowserRouter>
		</Provider>
	);

	onPageLoad(() => {
		let	div = document.getElementById('react');
		render(<App/>, div);
	});
}

main();

This has been a real killer, and my app size has literally been halved. I was thinking of using react-loadable for my Components, but this approach is a faster one that works on npm packages. react-loadable would be next.