State

One of the common challenges in a isomorphic application is the consistency of data between server and client. After the first page view (rendered server side), the Mithril SPA takes the control of the page, mounting itself into the document body and replacing the existing DOM with the one it generates. The first two important requirements are:

  • we want the real DOM generated by Mithril's virtual DOM to match the one rendered server-side, before mounting our SPA in the document.body replacing it
  • we want to avoid duplicated data fetching (the first on the server and the same again on the client)

A way to solve both issues is to adopt a shared state between server and client, where to keep track of the fetched data and anything else that has already been elaborated server-side in the controller of the main component.

In our approach we chose to use a simple custom state manager (app/stateman.js) to store and retrieve data. This also comes useful to achieve communication across different components.

In server routes

We add our state manager to the required modules at the beginning of app/server/index.js:

const stateManager = require('../stateman.js');

The Express routes need to be modified, passing down a new instance of the state at every request in the attrs:

Object.keys(routes).forEach((route) => {

    router.get(route, (req, res, next) => {
        const module = routes[route];
        const onmatch = module.onmatch || (() => module);
        const render = module.render || (a => a);

        // Istantiate a new state at every request
        const stateman = Object.create(stateManager);
        stateman.init();

        const attrs = Object.assign({}, req.params, req.query, { stateman });

        Promise.resolve()
            .then(() => m(onmatch(attrs, req.url) || 'div', attrs))
            .then(render)
            .then(toHTML)
            .then((html) => {
                res.type('html').send(html);
            })
            .catch(next);
    });
});

In components

We change our main components to use the state manager, which can be accessed via the vnode.state property:

oninit: vnode => new Promise((resolve) => {
    const stateman = vnode.attrs.stateman;
    if (!stateman.get('home.content')) {
        vnode.state.loading = true;
        resources.getSection('index')
            .then((content) => {
                vnode.state.content = content;
                stateman.set('home.content', content);
                vnode.state.loading = false;
                m.redraw();
                resolve();
            })
            .catch((err) => {
                vm.error = err;
                m.redraw();
                resolve();
            });
    } else {
        vnode.state.content = stateman.get(statePrefix + '.content');
        resolve();
    }
})

Instead of calling the resources.getSection() async fetch function directly as before, we first check if there's a hit for the key home.content in our state, using our state manager method stateman.get():

stateman.get('home.content')

Think it as a cache, where we avoid to fetch the data if we already have it. If the fetch is successful, we store the fetched data with our state manager method stateman.set():

stateman.set('home.content', content)

This won't affect the execution of the oninit on the server side because, as we said, we instance a new state at every request, so we will always have to perform the fetch. It will instead let the client avoid skip the fetch when the data is already available in the state, both if it comes from the server through the shared state or if it has already been fetched before in the SPA.

In server layout

On the server side, once the Promise of the async oninit of our main components is resolved, the state won't undergo further changes. At that point it's ready to be shared with the client, passing the data to the SPA.

As previously anticipated in the Layout section, this line of code does the trick:

m('script', `window.__preloadedState = ${vnode.attrs.stateman._getString()}`),

We are using here the stateman._getString() method of our custom state manager, which simply returns a stringified version of our state's object. In this way we can inject it as a string into the HTML and make it readable as a script by the SPA.

In client routes

As we did with the Express routes on the server side, we need to edit the Mithril routes on the client side to pass the state down to our main components. The difference here is that we are creating a single istance of the state at the init of our app to be used across the components:

const m = require('mithril');
const routes = require('./routes.js');
const stateManager = require('./stateman.js');
const resources = require('./resources.js');

const sharedState = window.__preloadedState || {};

// Get app state from server shared state
const stateman = Object.create(stateManager);
stateman.init(sharedState);

const clientRoutes = {};

const attrs = { stateman };
Object.keys(routes).forEach((route) => {
    clientRoutes[route] = {
        onmatch: (args, requestedPath) => routes[route].onmatch ? routes[route].onmatch(attrs, requestedPath) : routes[route],
        render: (vnode) => {
            Object.assign(vnode.attrs, attrs);
            return vnode;
        }
    };
});

m.route.prefix('');
m.route(document.body, '/', clientRoutes);

The stateman.get() and stateman.get() methods, when called on the common state manager passed down via vnode.state in our main components as seen before, can then be used as cache manager and also to implement cross component communication.