Meteor & React with SSR - Pushing initial state with EJSON

Pushing serverside component state to the client is called hydration. However, the methods used have some caveats, especially if you are working with rich content types like Date fields. In this tutorial I will explain how to leverage Meteor's EJSON (Extended JSON) to hydrate React components with initial state and rich content types.

To scope this article, we will not be adding fancy methods that require async stuff on the server. That's something for later on.. Its just to illustrate the problem and solution with Meteor's EJSON library as a replacement for JSON.

TL;DR.. Here's the resulting boilerplate code

How to push data to your clientside React components

We can utilize the window object and add a JSON stringified object to it. This will be sent along side with the serverside rendered HTML in script tags. On our clientside it will then automatically become available as a javascript object. See example below:

A logical place to start would be the boilerplate that I've created for a previous article about How to set up Meteor & React with SSR

In our startup/server.js file there's the following code:

import React from 'react';
import { renderToNodeStream } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';

import App from '../ui/App.jsx';

onPageLoad(sink => {
  sink.renderIntoElementById('app', renderToNodeStream(
    <App location={sink.request.url} />
  ));
});

The above code uses Meteor's onPageLoad which gets the Sink library as its parameter. Sink contains the request and response object and a couple of methods described in the Meteor docs about server-render.

Adding initial state to your server side

Lets extend the above code a bit with a bit of dummy state. Normally this state will be gathered from a bunch of different locations like Redux actions or a simple Meteor Method.

In this example we are going to push some site meta data into the app. As you can see, on the serverside part its just a matter of adding an additional parameter to our App component.

import React from 'react';
import { renderToNodeStream } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';

import App from '../ui/App.jsx';

onPageLoad(sink => {

  const initialState = {
    siteMeta: {
        owner: 'Chris Visser',
        version: '0.3.8',
        publishedAt: new Date('2018-05-10');
    }
  };

  sink.renderIntoElementById('app', renderToNodeStream(
    <App location={sink.request.url} initialState={initialState} />
  ));
});

Let's change our App component to simply do a console log for now.

import React from 'react';

export default ({ initialState }) => {
  console.log(initialState);
  return (
    <h1>Its working!</h1>
  )
};

Start your Meteor app and open it in the browser. You will notice that on our command line output all the information is given. However, if you open your developers console on the browser, it says 'undefined'.. That is because our clientside app doesn't have the initial state yet. We need a way to pass it to our bundle and ideally without doing an extra request.

Hydrating React Components with state

React's recommended way of hydrating the clientside is nicely explained in this article about React Client Side hydration. This method works nicely and is similar in Meteor, except, without the Express stuff. Let's first naively dive in and hydrate our client bundle 'the SSR Meteor way'.

Add the following line to the bottom of your onPageLoad function in startup/server.js

  sink.appendToBody(`
    <script id="preloaded-state">
      window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)}
    </script>
  `)

The above snippet uses sink to push a script tag into the bottom of our Server rendered HTML. It assigns a PRELOADED_STATE variable to the browser's window object. The initial state must be a string. This is why we stringify the initialState variable.

If you do a 'view source' in your app, you should see the stringified JSON string as part of the HTML body

Now in startup/client.js we need to grab this preloaded state window property and push it into our App component:

// Browser automatically parses the JSON string into a javascript object.
const initialState = window.__PRELOADED_STATE__;

delete window.__PRELOADED_STATE__; // Remove what we don't need anymore

If you now go to your app in the browser and open your console again, it should show you the state like on the serverside. There is a problem however! If you inspect the values of your state, you'll notice that the publishedAt field contains a date formatted as a string.. On the server however this is a Date object... Normally in Express based applications, you would have to 'pull the data trough a schema' - Meaning that all fields need to be parsed and transformed back into their rich equivalents. This process is error prone and leads to a lot of potential bugs.

Meteor however solved this problem for us by providing us with EJSON. (Extended JSON). It allows us to push rich content types to the client in the form of JSON and parses it back automatically for us! You can even define your own custom types if needed, but that's a different topic for now. Lets start using it.

Lets tweak our startup/server.js by adding the EJSON dependency. Also where we push the JSON data, we need to replace JSON with EJSON. Below should be the result.

import React from 'react';
import { renderToNodeStream } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';
// Add EJSON dependency
import { EJSON } from 'meteor/ejson'; 

import App from '../ui/App.jsx';

onPageLoad(sink => {
  const initialState = {
    siteMeta: {
      owner: 'Chris Visser',
      version: '0.3.8',
      publishedAt: new Date('2018-05-10'),
    }
  };

  sink.renderIntoElementById('app', renderToNodeStream(
    <App location={sink.request.url} initialState={initialState}/>
  ));

  // Push EJSON stringified state to the client
  sink.appendToBody(`
    <script id="preloaded-state">
      window.__PRELOADED_STATE__ = ${EJSON.stringify(initialState)} 
    </script>
  `)
});

Now lets open our browser console again and you will see a new value for the publishedAt field.

publishedAt: {$date: 1525910400000}

Its still not a Date object, but at least we now have an indicator that it should. We need to change how the EJSON string is parsed on our client side. Right now it just parses it as JSON.

The only thing that we need to do is to tweak the startup/client.js file:

import React from 'react';
import ReactDOM from 'react-dom';
import { onPageLoad } from 'meteor/server-render';
import { EJSON } from 'meteor/ejson';

// Stringify back the preloaded state into its original EJSON string form. 
// Then use the EJSON parser to parse rich content types
const initialState = EJSON.parse(JSON.stringify(window.__PRELOADED_STATE__));

delete window.__PRELOADED_STATE__; // Remove what we don't need anymore

onPageLoad(async sink => {
  const App = (await import('../ui/App.jsx')).default;
  ReactDOM.hydrate(
    <App initialState={initialState}/>,
    document.getElementById('app')
  );
});

In the above code we are doing one seemingly silly thing. We first stringify the preloaded state only to parse it back again.. What's the deal with that? Well your browser already uses JSON.parse by default to create a javascript object from our state. We don't want that. We need EJSON to parse our string. So we simply undo what the browser did and parse it using EJSON.

Now lets open the console again and your publishedAt field should have a Date value instead of a string value.

Chris Visser - Cloudspider
Software Developer & Strategist

Versatile, Passionate and involved for as long as there is coffee.

This is where I share my expertise about software development and software strategy in the form of articles, guides, tutorials and examples.

Find me on

My Boilerplates