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:
- You don't need to pre-define actions.
- There is no "string" magic
- 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.
Top comments (7)
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
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 :)
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.
Thanks for the comment Alexey. Yes, Cerebral allows time-traveling and the function-tree it has is very much Baobab inspired.
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.
The verbosity of the Redux connect example can be reduced by using the official API and some JS syntax:
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.
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.