March
2017

Cerebral 2

When Cerebral was released last year, it was a celebration of people gathering around an experiment. An experiment building a framework that understood the application it was running. A framework that would free us from getting lost building mental images of how the application works; letting us explore a graphical representation of the state, state changes, side effects, renders, and ultimately how these interact. With Cerebral 2 the experiment has matured into a framework that truly stands out in the JS jungle.

The status quo

In the JavaScript community today there are many frameworks to choose from, some more ambitious than others. Angular has now become an application platform, Ember has become a whole organization around developing applications with their framework. Vue also has a huge userbase. Even old grandpa Backbone still has many users. These frameworks are all great. They constantly innovate, support, and inspire their communities to build awesome products.

When React was released, it sparked vigorous innovation in state management. Views being functions that receive state and output UI is a beautiful concept which inspires musing on how we can best manage state without considering the UI at all. Solutions like Mobx and Redux are the most prominent out there in this space. They are not frameworks in themselves, but combined with React and other chosen tools we can put together our own framework.

So why would we choose one over the other? There is of course no simple answer to this. For some developers it can be as simple as choosing a framework based on its usage of JSX/Hyperscript vs traditional templates. Other reasons might be that “build our own framework” does not appeal at all; we want it all out of the box. Some developers favor writing as little code as possible, and do not see “magic” as a bad thing. Others want explicit code. The number of team members also affects the decision making. In the last year developer tools and type systems have also proven to be an important decision factor when choosing a framework.

Where does Cerebral fit in?

I saw a fantastic presentation by Preethi Kasireddy which compares Mobx with Redux. The reason I think the presentation is so good is because it nails the nature of application development and compares two very different approaches with their benefits and challenges. I will piggyback this presentation, adding Cerebral to the mix to see where it fits in. If you do not know Mobx or Redux I suggest you keep reading nevertheless as the code examples are quite simple and the concepts will matter to you no matter what framework you use. You might even find some approaches or tools you want to bring back to your existing framework and community. Please do, that is what open source is all about :-)

Learning curve

Every framework has new idioms, these being framework APIs, use of new JS APIs and/or patterns. This, in combination with the amount of “magic” introduced, affects the learning curve. Familiar code and magical code make a framework easier to learn. Angular introduced plenty of magic when it was released and many developers were dazzled by how easily they could make an input sync with some text on the page. But easy code does not mean code that is easy to scale and maintain; it is often in direct contradiction.

MOBX

Mobx has a familiar Object Oriented paradigm. This is what we know from older solutions like Backbone. Simply put, it means that we work with classes. We instantiate classes with state and methods for changing that state. This is a straight forward way of thinking about programming, but can get challenging when different class instances start to depend on each other and together need to express a complex flow of changes.


// Define state and state updates
class MobxState {
  @observable items = []

  addItem (item) {
    this.items.push(item)
  }
}

// Components
@observer
class Items extends Component {
  render() {
    return (
      <div>
        <ul>
          {this.props.store.items.map((item) => <li>{item}</li>)}
        </ul>
        <button onClick={() => this.props.store.addItem('foo')}>
          Add foo
        </button>
      </div>
    )
  }
}

// Pass state to components
const store = new MobxState();
render(<Items store={store} />, document.getElementById('mount'));

Mobx truly is magical in the way our components detect a need for render by tracking the access to observable properties. As a developer you normally do not have to think about how this works and as a result Mobx has a low learning curve.

REDUX

Redux has a functional approach. This means that we do not create class instances, we create reducers. A reducer basically holds an object representing state (much like a class instance), but it has no methods. Requests for change are passed into the reducers, and based on the type of change and its payload, the reducer typically uses a switch to return a brand new state object. Immutability is a strong concept in Redux which definitely has its benefits, especially in its simple render optimization, but also has its drawbacks when it comes to the learning curve.


// Define state and state updates
function ReduxState (state = Immutable.fromJS({items: []}), action) {
  switch (actions.type) {
    case 'addItem':
      return state.push('items', action.item)
  }

  return state
}

// Components
const Items = connect(
  (state) => {
    return {
      items: state.items
    }
  },
  (dispatch) => {
    return {
      onClick: (item) => {
        dispatch({type: 'addItem', item})
      }
    }
  }
)(
  function ItemsComponent ({items, onClick}) {
    return (
      <div>
        <ul>
          {items.map((item) => <li>{item}</li>)}
        </ul>
        <button onClick={() => onClick('foo')}>
          Add foo
        </button>
      </div>
    )
  }
)

// Pass state to components
const store = createStore(ReduxState)
render(
  <Provider store={store}>
    <Items />
  </Provider>,
  document.getElementById('root')
)

CEREBRAL

Cerebral is more functional than it is object oriented. Some ot the internals and API calls are object oriented, but at the application abstraction it is fully functional. Object oriented programming is very good for defining state and changing out state values. It is very expressive and straight forward. But as we start to get into the realm of cross domain state changes and side effects, a functional approach allows us to write declarative code. Declarative code can be read by the framework before hand, giving developer tools insight into what we want our code to do, before it is even run.


const controller = Controller({
  state: {
    items: []
  },
  signals: {
    itemAdded: push(state`items`, props`item`)
  }
})

// Components
const Items = connect({
  items: state`items`,
  itemAdded: signal`itemAdded`
},
  function ItemsComponent ({items, itemAdded}) {
    return (
      <div>
        <ul>
          {items.map((item) => <li>{item}</li>)}
        </ul>
        <button onClick={() => itemAdded({item: 'foo'})}>
          Add foo
        </button>
      </div>
    )
  }
)

// Pass state to components
render((
  <Container controller={controller}>
    <Items />
  </Container>
), document.querySelector('#app'));

Cerebral is not magical, we explicitly tell the framework (and ourselves) how everything is connected, but it is not as low level as Redux.

Boilerplate

How much code do we have to write? It is important to understand that less code does not mean better code. We could state that type checking is boilerplate, but it gives us guarantees and arguably more readability of our code. To avoid boilerplate we often use abstractions, but abstractions can hide logic in a way that makes it difficult for the next developer to understand what is really going.

MOBX

Mobx is what we call an implicit library. A good example of this is looking at how components render.


@observer
class Items extends Component {
  render() {
    return (
      <div>
        <ul>
          {this.props.store.items.map((item) => <li>{item}</li>)}
        </ul>
        <button onClick={() => this.props.store.addItem('foo')}>
          Add foo
        </button>
      </div>
    )
  }
}

In this component we are not defining what state the component depends on, it automagically understands that by accessing observable properties. It is less code to read, but it is harder to figure out what causes this component to actually update.

REDUX

Redux is very explicit about what state our components use. We basically create a factory that extracts state and actions to be dispatched:

const ADD_ITEM = 'ADD_ITEM'

function addItem (item) {
 return {type: ADD_ITEM, payload: item}
}

function Items ({items, onClick}) {
  return (
    <div>
      <ul>
        {items.map((item) => <li>{item}</li>)}
      </ul>
      <button onClick={() => onClick('foo')}>
        Add foo
      </button>
    </div>
  )  
}

connect(
  (state) => {
    return {
      items: state.items
    }
  },
  (dispatch) => {
    return {
      onClick: (item) => {
        dispatch(addItem(item))
      }
    }
  }
)(Items)

This contains a lot more boilerplate than Mobx. That said, it is more explicit about what state and state changes this component uses. We understand how the state gets to the component and therefore why it updates.

CEREBRAL

With Cerebral we are explicit, like Redux, but with less code. We connect state and signals where we need them. Unlike Redux, where you are encouraged to connect state at top level components and pass it as props to child components, you rather just connect state and signals exactly where it is going to be used. This improves readability.


connect({
  items: state`items`,
  itemAdded: signal`itemAdded`
},
  function Items ({items, itemAdded}) {
    return (
      <div>
        <ul>
          {items.map((item) => <li>{item}</li>)}
        </ul>
        <button onClick={() => itemAdded({item: 'foo'})}>
          Add foo
        </button>
      </div>
    )
  }
)

Again, the benefit of being explicit is that we know what the state dependencies are, and in Cerebral’s case, what signals the component can trigger. Since this is done through declarative code it can also be extracted and displayed in the devtools, helping us further understand what components depend on, without even running the render code.

Developer tools

There has been a revolution of developer tools in the React ecosystem. One thing is the React debugger itself, but when Redux got all its attention with immutabilty it opened up new possibilities. Especially the time travel debugger got a lot of attention. Time travel was actually one of the early experiments of the Cerebral debugger, almost 3 years ago, but it has ended up as a gimmick. The time travel itself is not the most valuable part; it is the history of state changes, and ideally how these changes came to be. What relates to our mental image of the application is where we find most value.

MOBX

Mobx has a developer tool that “does the job”, as Preethi says. We can investigate renders and what state properties a specific component depends on. This is done in the browser as an overlay.

REDUX

The Redux developer tools has gotten a lot of love. It can be used as overlay, as an extension or as a stand alone application. There are many different types of debuggers and we can combine them by our own preferences.

CEREBRAL

The Cerebral debugger is taken even further. Even though Redux lists mutations, we do not know how they relate to each other and how they came to be. In Cerebral we do not only get an overview of mutations, but also an overview of the complete flow of changes in our application. The debugger itself is a standalone application that allows us to connect to any JS environment, whether browser, server, React Native, Electron etc. We can even combine client and server side execution in a single operation flow.

Debuggability

When something goes wrong, how do we figure out what happened? Depending on the type of problem there are different approaches to debugging, but typically something happened when going from one state to the next. Being able to understand what actually happens when moving between application states is important. This insight can come from reading the source code and/or having devtools to help us visualize this.

MOBX

With Mobx’s magical nature it can be hard to track down bugs. Because state transitions happens “behind the scenes” it can be difficult to understand what exactly happens reading the code. Also the fact that changes can happen anywhere does not make things easier. That said, Mobx can be forced into a one-way-dataflow and the devtools helps us understand how a component renders.

REDUX

Redux is very explicit about how things are connected so it is easier to debug by reading the code around where the problem occurs. Also the fact that the state of reducers and changes to that state is isolated also helps a lot. The ability to skim through the mutations log in the developer tools is also a great benefit.

CEREBRAL

Since Cerebral is explicit about its state dependencies, where changes can occur, and has a one-way dataflow, it has the same benefits as Redux. On top of that, we have the devtools giving us a mental image of how state changes occur, with the ability to filter out specific state changes. This makes debugging application logic a great experience. Where Cerebral truly stands out though is the fact that we can see more than individual state transitions; we can see a flow of state transitions and side effects related to a specific event in the application.

Predictability

This is related to the previous point. Does our code behave the way we expect? One of the introductions to Flux was the counter on the Facebook notifications button. There was an issue where it popped up when it was not supposed to. They had many iterations trying to fix the bug, but it kept coming back. This is why Flux was introduced. It gave a predictable way to update state using the concept of one-way dataflow. All requests for changes targets “the top of the application”, which changes the state, and then the components render. The UI is a direct result of the current state of the application.

MOBX

If we do not force one-way-flow in Mobx it can become unpredictable as changes can occur anywhere.

REDUX

Redux, with its explicit definition of what a state change can be and how it is made, is the “king of predictability”. Based on the ideas of Flux, Redux has become THE implementation of Flux and is therefore very predictable.

CEREBRAL

Cerebral is also built on the concepts of Flux. The components only trigger signals that say: “This happened”. It is then up to Cerebral to run the mutations and side effects as defined. Since we define the whole flow in one signal Cerebral is very predictable. Changes are not divided into different parts of our code. Everything happens in one place, composed together, and this composition is also displayed in the debugger.

Testability

Some developers are very aggressive with testing, 100% coverage is the goal. Other times testing takes away too much time due to regular changes in the application itself, typical for startups. We can test components, we can test a state change, a flow with side effects or just a function computing some data. No matter what we test, it is important that things are not intertwined, like state changes are intertwined with component render, etc.

MOBX

If we allow Mobx to make changes anywhere and class instances are passed into other class instances, the code becomes harder to test. That said, with testing in mind it is perfectly possible to make Mobx testable using actions and planning out our domains to be as isolated as possible.

REDUX

Redux is basically pure functions. The actions just receive input and return an object. The reducers works the same way, only with state. This makes Redux highly testable. That said, when we get into side effects we are no longer in pure function world and things get harder to test.

CEREBRAL

Testing state changes is just a tiny bit of the story. Tests should really be done on the flow of changes in our code, so called integration tests. When a user clicks here, ajax requests are made, state changes are made etc. what state do we end up with at the end? Cerebral separates side effects from execution, even state changes. The functions in the signals get one input, called the context, and this context can easily be mocked during testing… even for a whole signal execution (integration test). Cerebral also has a set of helper tools to write less boilerplate for tests.

Modularity

As developers we tend to favor isolated pieces of code, modules. The challenge though is that, particularly in frontend code, these modules need to talk to each more often than not. They need to access each other’s state and trigger logic “within” each other. Planning out how these relationships should work and avoiding circular dependencies can be problematic. Decoupling with events can also decrease the readability of how things relate to each other.

MOBX

Mobx uses classes: state and methods for changing that state and doing side effects are all contained inside a class. In that sense Mobx has really good modularity. The challenge though is when these class instances need to access each other’s state or trigger each other’s methods. It can be difficult to coordinate.

REDUX

Redux does not really have a concept of modularity. We define our actions somewhere over here and our reducers somewhere over there. The great thing about this, is that any action can trigger any state changes in any reducer, and any reducer can react to any action.

CEREBRAL

Cerebral has a concept called modules. This is a way for us to structure our application, but without isolating signals and state: any signal can change and grab any state. Also any signal can compose its logic from other signals. That means Cerebral is highly composable and modular, without the isolation. It is difficult to go wrong planning out the domains and there is basically no risk having circular dependency issues. There is no need to pass class instances around to get access to what we need.

Scalability / Maintainability

Writing the 500th line of code and the 10000th are very different. When the application grows it becomes more important to keep things simple, rather than easy. Simple means having clear concepts and responsibilities. This part of the code handles UI rendering, this part handles request for state changes and this part does the state change. It is tempting to do all of this inside one component for example, but it quickly becomes complex when 50 components all have their own internal state, side effects and state changes, trying to “reach into” each other when necessary. When applications grow we need to have a good separation between these concepts.

MOBX

Mobx is very easy to get going with, but it does not force us into a strict pattern of where to request state changes, where to make those state changes and side effects. It can happen anywhere. That makes Mobx, without good discipline, less than ideal for scalability and maintenance.

REDUX

Redux has clear concepts of what components are for, that actions need to be triggered to request state changes, and reducers are where state changes happen. This makes Redux highly scalable and maintainable. That said, there are no strict opinions on how to handle side effects. There are tools to help us with this though.

CEREBRAL

Cerebral has clear concepts of what goes where. Our components should ideally not handle any state. There are always exceptions for complex UI updates, which is the case for any framework, but for the rest, all the state goes into Cerebral. We can only change state by firing off a signal, and the signals also has the logic for running side effects, composing it all together in a coherent flow. Scaling Cerebral is adding new state and signals, or new modules for structure purposes. It is easy to onboard new team members as they quickly get the mental image of how the application works by clicking around in the UI and looking at the debugger.

Summary

When looking at Preethis presentation it struck me that Cerebral is this balance between Mobx and Redux. It gives us the predictability, explicitness and great devtool experience of Redux, but with less boilerplate and a lower learning curve. This is not saying that Cerebral is the perfect solution and you do not need Mobx or Redux. It is just an alternative that might fit you better if you think Mobx is too magical and “radical”, and Redux is too much boilerplate and “conservative”.

Thanks for reading through and feel free to check out more about Cerebral on the official Website.

blog comments powered by Disqus