The Anatomy of a Completely Decoupled Flux-like Architecture

In the React world we need to make a lot of choices before we have a running application. React being “just the view”, we still need to decide how to do our network I/O, storage, application logic and routing.

A lot of React projects choose a Flux-like architecture and use libraries such as redux and react-router. Some libraries provide bindings to ease the integration between the state container and React (redux-react, fluxible), or implement what is not necessarily UI using React components (react-router). An alternative approach is to avoid that kind of integration and keep everything decoupled, controlling and wiring up dependencies yourself.

We’ll take a step back and look at what building blocks there are in a modern Flux-like application, and how a minimal setup could look like if we would completely decouple everything.


A minimal reference implementation is on its way. Meanwhile you can have a look at the (by now much larger and more complex) React Blueprint that follows the same principles.

Flux-like means that we…

…build our application following certain principles;

  • Event handling and rendering is separated from application logic. We will probably use a library like React or Riot.
  • We care about unidirectional data flow and feed our rendering with an immutable application state object, either by strict convention or forcing it using Immutable.js, Mori or similar (recommended).
  • When our application state object changes we rerender the entire component tree. React makes this perform with VDOM diffing and shouldComponentUpdate checks (which are easy with immutable data).
  • The implementation of our application logic will update our application state object, which on change will need to be rerendered again.

Which results in the following building blocks:

  • Renderer. Something that takes an application state object, some event handlers, renders the complete UI and lets us know when the user did something relevant.
  • State container. Something that holds the latest version of our application state, allows application logic to update this state and lets us know when state changes.
  • Actions. Implementation of logic that either updates application state or describes how to update this state. Implements I/O and application logic.
  • Router. Something that handles our (browser) history and knows what needs to happen when the URL changes. It can convert back and forth between an URL and functional route definitions.
  • Bootstrap. Some sort of bootstrapping code glueing all these things together:
Close to the classic Flux diagram

Renderer

A simple version of a decoupled renderer can look like this:

type Actions = { [ handlerName ]: Function };
function renderer(state: Object, actions: Actions, renderServices: Object, element: DOMElement): void;

The state argument is our (preferably immutable) state object that describes everything that needs to be rendered on a functional level. The state object may exclude some UI-only state in case it’s of no interest to our application logic and leave that to the renderer, like a particular animation state. But basically the state object will be able to describe any renderable state of the application.

{
title: '',
content: '',
sidebarOpen: false,
errorMessages: [],
replies: [
{ authorId: 12, message: '...' },
...
],
...
}

The actions argument is an object containing plain JavaScript functions that the rendered UI can just call without having any knowledge of the machinery behind it. Fire and forget. Example:

actions = {
openSidebar(): {
// secret implementation
},
setTitle(title): {
// secret implementation
}
}
// Imaginary React code
<div onClick={actions.openSidebar} >...
<div onClick={actions.setTitle('Alt')} >...

Whoever constructs the action object will make sure those functions end up updating application state, that the renderer then has to rerender with.

The renderServices argument is a way to inject dependencies into the renderer. For example, maybe the renderer needs to render URLs defined by the router:

renderServices = { getUrl: router.getUrl.bind(router) }
renderer(..., actionServices, ...);

Here the renderer doesn’t need to know about any specific router and can just call getUrl as some sort of helper function.

The implementation of the renderer itself can call React.render passing the arguments as props. React components use these props as they see fit and pass them on to their children. Notice how React-agnostic our renderer is; you could use anything else.

function renderer(state, actions, services, element) {
ReactDOM.render(
<Root state={state} actions={actions} services={services} />,
element);
}

Imaginary Root component:

const Root = ({ state, actions, services }) => (
<div>
<Navigation
...
setUrl={actions.setUrl}
getUrl={services.getUrl}
/>
<Article replies={state.replies} />
<Button onClick={actions.openSidebar} />
</div>
);

We now have a completely decoupled renderer with the flexiblity to hook up any kind of external state container or routing solution. This keeps rendering simple: we render the application state and wire the UI to make action calls on interaction.


Using “service objects” to pass around dependencies is the poor man’s dependency injection but it’s simple and it works. For more serious work you can consider a real DI container like scorpion.

State container and actions

We want the state container to manage our application state object and notify us when anything changes, so we can rerender the application with the updated state.

type StateContainer = { actions: Object, getState: Function }
function createStateContainer(initialState: Object, onChange: Function): StateContainer

Our actions should call plain functions that implement the particular functional action in whatever way makes sense for the particular state container implementation. Create Redux Action Objects in case of Redux, for instance:

// module action.js
export function showSidebar() {
return { type: 'SHOW_SIDEBAR' };
}
export function setTitle(title) {
return { type: 'SHOW_SIDEBAR', payload: title };
}

To be able to use these in a renderer, these functions should be curried to hide their implementation and dispatch their result directly to the state container. An example implementing the state container with Redux:

import mapValues from 'lodash/mapValues';
import { createStore } from 'redux';
import * as actions from './action';
function bindActions(actions, store) {
return mapValues(actions, (action) => {
return (...args) => {
store.dispatch(action(...args));
};
});
}
function createStateContainer(initialState: Object, onChange: Function) {
const store = createStore(reducer, initialState);
const boundActions = bindActions(actions, store);
store.subscribe(onChange);
return { actions: boundActions, getState: () => store.getState() };
}

This is a decoupled state container. We now use Redux but we could use anything else that runs standalone. The actions object contains the curried “fire and forget” functions we later pass to the renderer and the router.

const { actions } = createStateContainer(...);
actions.setTitle('Value'); // magically updates the app state object

Optionally you could provide createStateContainer with an actionServices object in case you need to pass dependencies (just like we did with the renderer).

Router

There are many different ways and libraries to do routing. If we pick a standalone library allowing to do routing outside of React, we could decouple and wrap it in something like:

type Router = { getUrl: Function };
function createRouter(routeServices: Object): Router;

The routeServices object could contain action functions to actually make the application state object changes happen the moment the URL changes:

const routeServices = {
setTitle: actions.setTitle,
setScreen: actions.setScreen
};

This way the implementation of our route handlers can simply call these functions and let the state container and renderer worry about the rest. The Router’s getUrl we can pass to the renderer using the renderServices object:

renderServices.getUrl = router.getUrl.bind(router);

Bootstrap

This is where we glue everything together:

// bootstrap.js
import State from './state';
import renderer from './renderer';
import createStateContainer from './state-container';
import createRouter from './router';
const element = document.getElementById('root');
const initialState = new State();
const renderServices = {};
const routeServices = {};
const router = createRouter(routeServices);
const { actions, getState } = createStateContainer(initialState, () => {
renderer(getState(), actions, renderServices, element);
});
routeServices.setTitle = actions.setTitle;
routeServices.setScreen = actions.setScreen;
renderServices.getUrl = router.getUrl.bind(router);
// first render
renderer(initialState, actions, renderServices, element);

In case you want to do server side rendering you will need a separate bootstrap for the server, and do some more work in the browser bootstrap to sync application state.

Decoupled approach

Decoupling can be set up and implemented in different ways, but we don’t need to use bindings or deep integration to set up a Flux-like architecture. Decoupling each part and controlling the bootstrap gives us control and flexibility while keeping all concerns separated.