Going isomorphic with Reactive Switchboard

There’s an elephant in the room and everyone’s talking about it. If you’ve worked on a single page web application over the last few years, you’ve most likely spent time wondering how come building an isomorphic web application seems so complicated. While there are solutions, most of them require restrictive architectural choices or other excessive trade-offs. The bottom line seems to be that if you want to get all those juicy benefits of an isomorphic app, you need to design for it right from the start.

Since I started working on Reactive Switchboard, I’ve wanted to have a simple answer to the question “how do I render my app on the server side?” It shouldn’t be a question you need to plan on answering from the start of the project, but rather when it becomes relevant. That’s why I’m glad to announce that Reactive Switchboard 2.0 is going to ship with the ability to easily render your app server-side.

What’s the problem?

If you’ve ever looked into the problem of server-side rendering React applications, you’ll know that rendering a component into a string is simple. In fact, it’s a single line of code:

require(‘react-dom/server’).renderIntoString(<MyComponent/>)

This works fine as long as your component does no state handling of its own: everything that it renders is passed in as a prop. While many developers advocate this approach, it also forces all state handling to exist on the routing level. In an increasingly complex application, that can lead to some truly monstrous routing code.

Ideally we want to declare in the context of the component what kind of data it’s dependent on. I’ve become used to writing code like this:

apiContainer('products', function Component({ products }) {...})

This works great on the client-side; if products haven’t loaded yet, we can simply display a loader instead of the component’s content. On server-side things aren’t quite that simple for one reason: there is no lifecycle. You only render the component once — there’s no waiting for the request to finish. Basically this means that you need register the state of every that relies on outside data in ‘componentDidMount’, the only lifecycle method that gets called during ‘renderToString’. Then you need to figure out when it would be safe to try again. Needless to say this leads to some complicated code.

Thinking with signals

In Reactive Switchboard we define a component’s dependencies as a collection of streams:

switchboard.component(({ signal }) => ({
    products: signal({status: ‘loading’}, loadAsyncValue(...)),
user: signal({status: ‘loading’}, loadAsyncValue(...))
}), function Component({ wiredState: { products, user }}) {…})

This gives us a critical benefit: the whole application’s state can be observed as a stream. To do that, we need to collect all stateful components from our render tree. I chose to use the React context for that:

let inject = (element, switchboard, wiredStates) => 
React.createElement(React.createClass({
childContextTypes: {
switchboard: React.PropTypes.object,
wiredStates: React.PropTypes.array
},
        getChildContext() {
return {
switchboard,
wiredStates
}
},
        render() {
return element;
}
}))

The ‘inject’ function takes a component and adds the ‘switchboard’ and ‘wiredStates’ arguments to its child components’ context. They can then be used to collect important data about the components

componentWillMount(){     
...
this.context.wiredStates.push(this.savedWiredState);
}

After the component tree has rendered, ‘wiredStates’ will have a list of all state streams used by the application. Since we can access the entire application’s state, validating it becomes a question of writing a function.

let isDependencyValid = (value) => value.status === 'done'

So to find out if our application state is valid, we simply call ‘isDependencyValid’ with every dependency in our application.

let isAppInvalid = 
applicationValues.filter(isDependencyValid).length > 0

Since every dependency is a stream, we can simply observe them to find when they enter a valid state. We’ll therefore know that the application enters a complete state when all dependencies produce a valid value:

// produces a value when all invalid streams
// have produced a valid value
kefir.zip(invalidDependencyStreams.map(
(it) => it.filter(isDependencyValid)
)).take(1)

When the current dependencies become valid, we can try rendering again and see whether the application is ready to be sent to the client or we need to load more dependencies. With proper caching, most of your views should render with a single pass.

Applying the methods discussed above, rendering an isomorphic app using Reactive Switchboard boils down to this:

board.validateSignals(
react.createElement(app),
isDependencyValid,
(renderedHtml) => response.send(renderedHtml)
)
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.