Merrick Christensen's Avatar
I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.Leonardo Davinci

Single State Tree + Flux

2015-08-30

When people ask me how I build user interfaces these days they often stare back at me with wide-eyed shock when I tell them I manage all my state in a single tree using Flux to populate it.

Wait a minute, you store everything in a single tree? Like one giant variable?

Yup. And it's awesome.

Really?

Totes McGoats

What is the "single state tree" anyways?

In order for me to make my audacious claims of why I think this approach is awesome, let me first explain the approach. It's really quite simple, all of your application's state is managed in a single tree. The tree is populated using a concept called "actions".

What are actions?

Actions are serialized representations of events that get dispatched your "store", your stores job is to manage that single state tree. Some examples of actions:

The results of an HTTP request:

{
  type: 'FETCH_COMMENTS_SUCCESS',
  payload: {
    comments: [...]
  }
}

A user interaction:

{
  type: "SAVE_COMMENT";
}

Or any other type of state changing mechanism.

What is a store?

Your store receives each action and figures out what state to derive from it. For example, the system make a request to get the latest comments, it then dispatches an action, the store then says:

Ahhh, FETCH_COMMENTS_SUCCESS, I know precisely what to do with you, I save your comments into cache and notify my observers that state has changed.

So what does this process look like for a typical "Download and Show Data" requirement? Well first we have what we call an "action creator" which is really just a function that creates an action object, or a serialized representation of a state change, and dispatches it to the store. So it all starts with an action creator:

function getComments(dispatch) {
  $.ajax({
    url: "/post/single-state-tree/comments",
  }).then((comments) => {
    var action = {
      type: "FETCH_COMMENTS_SUCCESS",
      payload: {
        comments,
      },
    };

    dispatch(action);
  });
}

We then have a store with a dispatch function that manages all state changes.

function dispatch(action) {
  if (action.type === 'FETCH_COMMENTS_SUCCESS') {
    // Aww yes my good sir, I know precisely what to do with you.

    // Set next state...
    state = Object.assign({}, state, { comments: action.payload.comments}};
  }
}

These two are then combined accordingly to dispatch the action.

getComments(dispatch);

Typically dispatch is associated with a store, I'll get into that in just a bit. Anyways, subscribers to state changes are then notified, "Hey Miss, your state looks different!"

What is a single state tree?

A single state tree is a single tree that represents your state. This approach is different than traditional flux in that you have a singular store which manages all of your states in one mega tree. First, though let's recap some of the benefits of Flux.

Benefits of Flux

  1. Using actions to represent state changes provides a serializable data format for all your systems state changes.
  • Ever wondered how your application got into a particular state? Wonder no more, we now have a frame by frame replay of every state change your system has ever experienced.
  • This frame by frame replay is shareable, imagine a bug report with a downloadable "reproduce" file. You simply play the "reproduce" file and get the same bug.
  • This frame by frame replay is analyzable. Imagine recording all your user tests and using analysis tools to get deeper insights into how people interact with your system. Also, optimization opportunities anyone?
  1. All of your state changes are passed through a single mechanism.
  • A developer can walk into a system and know every potential state changing piece of code there is by looking at your action's constants.
  • Developers can author "middleware", or functions that each action is passed through. This enables things like logging each state change, having different types of actions such as thunks or promises, implementing analytics, the possibilities are only as limited as our creativity.
  • Developer tooling hook into every state change, holy awesome.
  1. Synchronous UI
  • UI is rendered synchronously, every time. No more this state change caused this state change, caused this state change, caused this state change. Just here is the current state, what does the UI look like with this state? Just think about it, your UI as a pure function... ever heard of UI so easy to test? Me either.
  1. Decouples actions from state changes. This means you can have one action make multiple state changes. For example, HTTP responses are decoupled from state changes. This is useful for endpoints that contain nested entities. For example, say you download all of the comments and they come back with nested user objects, with flux you can simply populate the user section of the tree, next time someone asks for that user, you can skip the request because you know you have the sufficient state.

  2. The questions, "What is my state?" & "When does my state change?" is answered simply instead of being littered throughout your application code.

Ok, that all sounds great but at what cost! I hear you gasping...

But, but, but this approach seems unwieldy and infeasible.

Does it, dear friend? It's not. There are tools to rope in the hard parts. Here is an example of my tool of choice for leveraging this technique, called Redux.

Redux

Redux has a remarkably simple interface. And its job is solving precisely what this article is all about, a single state tree managed by actions. Let's start out by making a store, in redux this function is called createStore.

Creating a Store

import { createStore } from "redux";

let store = createStore(handler);

When we call createStore we give it our action handler or our reducer. The reducers job is to take the current state and the action and return the next state. Following our example above let's write a simple reducer.

function reducer(state = {}, action) {
  if (action.type === "FETCH_COMMENTS_SUCCESS") {
    return Object.assign({}, state, {
      comments: action.payload.comments,
    });
  }
}
let store = createStore(reducer);

Writing a reducer like this can get a little unwieldy because you are managing the structure of your tree yourself, thankfully Redux offers a little utility function called combineReducers, its job is to take multiple reducers and manage the tree structure for you. It can be as nested as you like but we'll demonstrate a flat tree below.

let reducerTree = {
  comments: (state = {}, action) {
    if (action.type === 'FETCH_COMMENTS_SUCCESS') {
      return action.payload.comments;
    }
  },
  users: ...,
  posts: ...
};

let store = createStore(combineReducers(reducerTree))

Now our comments reducer is just the reducer for comments, our user reducer is just the reducer for users etc.. Our state tree would look like this:

{
  comments: {},
  users: {},
  posts: {}
}

Notice how it matches the reducerTree we provided to combineReducers?

Since we have our reducer wired up when we call dispatch on the store with that particular action, the store's state tree will change to a copied state tree with the comments included. But how do we access this state tree? Well its pretty simple really, we call getState.

let state = store.getState();

This can be called at any point in time to retrieve your applications current state but you typically call it onSubscribe. Which lets you listen for changes to the state and do something accordingly, you know, like render your interface again.

store.subscribe(() => {
  let state = store.getState();
  render(ui, state);
});

This gives you simple, synchronous renders, where each render is not a mix of changes over time but a singular snapshot of state at a given point in time.

Benefits of Single State Tree

  1. Improved developer tooling. Since all your state exists in one location, other parts of your application can be reloaded without blasting your state. Gone are the days of reloading your entire application, clicking different things to get your application into that state you are working on and testing your changes. Instead, you simply reload that one file and your application's state stays intact. You can do this kind of hot reloading with different tools in the ecosystem.
  2. Shared cache across your system. When one section of your application downloads a user, its there for the other pieces of your application that use that user, no HTTP request required.
  3. Your entire applications state can be viewed in one structure (and shared as one structure) see benefits of Flux above.
  4. The majority of your applications state management are pure functions, hello testability.
  5. Your applications state can be simply bootstrapped from the server, hello server-side rendering.
  6. State changes are predictable.
  7. Undo & Redo are practically free.

Further Reading