SSR with server-render: A potential strategy

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?
1 Like

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?

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.

2 Likes