loading...

CerebralJS

reflog profile image Eli Yukelzon Originally published at blog.reflog.me ・3 min read

I want to preface this post with the following disclaimer:

I am not a fan of Redux. It became a de-facto standard in state management of React apps and seems to be working great for a lot of people, but I find it to be very verbose and hard to work with.

Ok. Now that it's out of the way, let's see what else exists in the world today that can help us maintain our application state and keep our sanity.

The project I'm going to discuss is called Cerebral and it was created by Christian Alfoni, Aleksey Guryanov and many others specifically to address the downsides of Flux and Redux.

I highly recommend reading Christian's introduction article to Cerebral 2 to get a sense of the main differences between the frameworks.

In this post I'm going to make a small introduction to Cerebral by comparing the basic Counter example written using Redux to one in Cerebral.

In upcoming posts I'll start introducing more advanced concepts and that's where things will start getting really fun :)

Redux Counter

A simple Redux application consists of:

Entry point



import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import counterApp from './reducer'
import Counter from './Counter'

let store = createStore(counterApp)

render(
  <Provider store={store}>
    <Counter />
  </Provider>,
  document.getElementById('root')
)


Reducer



export default (state = 0, action) => {
  switch (action.type) {
    case 'INCREASE':
      return state + 1
    case 'DECREASE':
      return state - 1
    default:
      return state
  }
}


Main component



import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { increase, decrease } from './actions'

const mapStateToProps = (state) => {
  return {
    count: state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onIncrease: () => {
      dispatch(increase())
    },
    onDecrease: () => {
      dispatch(decrease())
    }
  }
}

const Counter = ({ onIncrease, onDecrease, count }) => (
  <div>
    <button onClick={onIncrease}>+</button>
    {count}
    <button onClick={onDecrease}>-</button>
  </div>
)

Counter.propTypes = {
  onIncrease: PropTypes.func.isRequired,
  onDecrease: PropTypes.bool.isRequired,
  count: PropTypes.string.isRequired
}


export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)


Actions



export const increase = () => {
  return {
    type: 'INCREASE'
  }
}

export const decrease = () => {
  return {
    type: 'DECREASE'
  }
}

And it works like follows: you define your actions separately, then define the 'reaction' to those actions in the reducer, i.e. how the state will be affected. Then you connect the component to the state.

Here's the full project on WebpackBin

Cerebral Counter

A simple Cerebral application consists of:

Entry point



import React from 'react'
import {render} from 'react-dom'
import {Container} from 'cerebral/react'
import controller from './controller'
import App from './App'

render((
  <Container controller={controller}>
    <App />
  </Container>
), document.querySelector('#app'))

Controller



import {Controller} from 'cerebral'
import {set} from 'cerebral/operators'
import {state, string} from 'cerebral/tags'

function increase ({state}) {
  state.set('count', state.get('count') + 1)
}

function decrease ({state}) {
  state.set('count', state.get('count') - 1)
}

const controller = Controller({
  state: {
    count: 0
  },
  signals: {
     onIncrease: [increase],
     onDecrease: [decrease]
  }
})

export default controller

Main component



import React from 'react'
import {connect} from 'cerebral/react'
import {state, signal} from 'cerebral/tags'
export default connect({
  count: state`count`,
  onIncrease: signal`onIncrease`,
  onDecrease: signal`onDecrease`
},
function App ({ onIncrease, onDecrease, count }) {
  return (
   <div>
    <button onClick={() => onIncrease()}>+</button>
    {count}
    <button onClick={() => onDecrease()}>-</button>
  </div>
  )
})


And it works like follows: you define a controller that contains a state and a list of signals that are handled by it. Then you connect a component to specific state elements and signals and use them directly.

Here's the full project on WebpackBin

As you can see there are quite a few differences here:

  1. You don't need to pre-define actions.
  2. There is no "string" magic
  3. Code is much less verbose

And what you've seen here is just the absolute tip of the iseberg. Cerebral provides so much more! I hope to get into all of it in upcoming posts.

Discussion

pic
Editor guide
Collapse
mpjme profile image
Mattias P Johansson

First of all, Cerebral is some interesting work, and I don't want to put interesting experimentation down. However, since you're not above putting Redux down, I figure that you wouldn't mind some critique.

All of Alexey's points are valid, but I'd like to make a few more.

It's important to note that Redux has quite a healthy ecosystem that we're throwing away if we're picking an alternative. This doesn't mean that we shouldn't, but if we're proposing alternatives, we need to propose ones that offers very significant improvements, not just incremental ones, and we also need to be well-educated in our critique.

First of all, I'm not entirely sure what you mean by point 1. Maybe I'm misunderstanding, but there is no need to pre-define actions in Redux. It's often done due to convention because many people working considers it beneficial to have a little library/index of all the actions that are flying around, but action creators you define under "Actions" are not strictly necessary - I personally just dispatch the action directly. I.e.


dispatch({ type: 'INCREASE' })

instead of




I also feel that you've thrown out a lot of the testability benefits of Redux, which to me is the biggest benefit it brings. The "string magic" (magic is a bit of hypebole here in my opinion, but I digress) is what makes Redux so easy to unit test - everything being these simple objects that you can just assert. Often you can get away without using any mocking/stubbing at all, which makes your tests a lot less painful to deal with. The same thing goes for reducers - since they are just simple functions that take input and output, you can write very simple tests that don't require mocking or any kind of special libraries. The state.set/get is significantly harder to deal with in this context.

Verbosity is not a particularly compelling reason to pick tools in my experience. Writing new code is a very small part of the workload that involves writing an application, and it's a lot more important that the code is easy to reason about. I do not feel that Cerebral offers any improvements in this area - mapStateToProps might be a verbose mouthful, but it's a verbose mouthful that one can paste into google an immediately get a result. This is often the problem with tooling that tries to battle verbosity, it ends up sacrificing discoverability on the altar of terseness. Haskell does this mistake as well, it's absolutely positively ungoogleable. 



Collapse
reflog profile image
Eli Yukelzon Author

Thank you for a detailed reply. I do understand your point, and btw, I plan to write about Cerebral's testability, it's actually one of it's strengths :)

From all my exposure to Redux-based projects I felt that the code that Redux inspires becomes very verbose. Maybe it's just me. And there is a reason why people keep writing action-helpers, thunks, sagas, etc and there is a whole wide eco-system around Redux - it's very hard to make something easy and readable JUST with it.

But again - I never claimed to be an expert in the field, just sharing my experience :)

Collapse
batmansmk profile image
Bat Manson

The verbosity of the Redux connect example can be reduced by using the official API and some JS syntax:

import { connect } from 'react-redux'
import { increase, decrease } from './actions'

/** same component here **/

export default connect(
  state => ({count:state}),
  {decrease, increase}
)(Counter)

No need to create mapDispatchToProps intermediate functions. It is less verbose than cerebraljs :).

It has an edge over the presented Cerebral equivalent as it uses the correct JS references instead of some some template string + string references which removes typings, linter features, autocomplete, IDE functions etc.

If you want to be fair, you also have to add propTypes in both examples, not only in the redux examples.

Collapse
alexeyzimarev profile image
Alexey Zimarev

You do not any "string magic" for Redux. You can easily create constants for all your actions and use them instead of strings.

Redux performs operations based on events. Events in general is a valuable source of information. This was of handling operations allows Redux to have the timeline and the "time machine"-style development tools. I might be wrong assuming this is not the same for Cerebral since I don't know the background of signals.

What also bothers me is that you just declared "no string magic" and I clearly see you do not have store as an object but you have state as a bag, using strings to get and manipulate the state. I am not sure how is this better. The state is also mutable, unlike immutable store in Redux, which is one of the best features of Redux. Immutable store allows writing functional-style code without side effects, basically the idea was taken from Elm.

Collapse
reflog profile image
Eli Yukelzon Author

Thanks for the comment Alexey. Yes, Cerebral allows time-traveling and the function-tree it has is very much Baobab inspired.

Collapse
guria profile image
Aleksey Guryanov

Cerebral 0.x was started also on Elm ideas even before @gaeron decided to make a redux.
In Cerebral 1 we were used models based on Baobab and ImmutableJS. But in Cerebral 2 we were managed to have time-travelling without overhead brought by immutable models.

Collapse
reactiveintent profile image
Mvsica Donvm Dei

Yes, the world needs more than Redux! I Recently investigated function-tree, redux-logic, redux-observable, ngrx and more. Function-tree alone is taking a bold new and independent step, yet I'm aiming on for a pure RxJS centric store and reducer solution. Presently, I'm stepping over the land mines as I find my way to a whole new reactive functional world.