SubX, Subject X, Reactive Subject. Pronunciation: [Sub X]
SubX is next generation state container. It could replace Redux and MobX in our React apps.
- Schemaless, we don't need specify all our data fields at the beginning. We can add them gradually and dynamically.
- Intuitive, just follow common sense. No annotation or weird configurations/syntax.
- Performant, it helps us to minimize backend computation and frontend rendering.
- Developer-friendly: forget actions, reducers, dispatchers, containers...etc. We only need to know what is events stream and we are good to go.
- Based on RxJS, we can use ALL the RxJS operators.
- Small. 300 lines of code. (Unbelievable, huh?) We've written 5000+ lines of testing code to cover the tiny core.
In this article, we only focus on one of its features: developer-friendly. We will use concrete examples to demonstrate why is SubX more developer-friendly than Redux & MobX
Being developer-friendly is somewhat subjective. Different person might have slightly different opinions. Here I list two critera, if you have better alternatives, please leave comments to let us know.
- The fewer lines of code, the better.
- The fewer new concepts, the better.
This is pretty self-explanatory. The fewer code, the easier to write, read & maintain.
In theory, you can use some tool to minimize your code so it looks very short but readability suffers. So we define two additional rules here:
- No line could be longer than 120 characters.
- Code must be readable by human beings.
The fewer new concepts, the easier to learn and master, thus more developer-friendly.
If two approaches could achieve the same goal, the one with fewer new concepts win.
It is a simple app. It's so simple that we only use it as appetizer.
Here is the latest source code. I paste the JavaScript code below:
/* global SubX, ReactSubX, ReactDOM */
// store
const store = SubX.create({
number: 0,
decrease () {
this.number -= 1
},
increase () {
this.number += 1
}
})
// component
class App extends ReactSubX.Component {
render () {
const store = this.props.store
return <div>
<button onClick={e => store.decrease()}>-</button>
<span>{store.number}</span>
<button onClick={e => store.increase()}>+</button>
</div>
}
}
// render
ReactDOM.render(<App store={store} />, document.getElementById('container'))
Here is the latest source code. I paste the JavaScript code below:
/* global React, Redux, ReactRedux, ReactDOM */
const { Provider } = ReactRedux
// reducer
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
const initialState = { number: 0 }
const reducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return { ...state, number: state.number + 1 }
case DECREMENT:
return { ...state, number: state.number - 1 }
default:
return state
}
}
// store
const store = Redux.createStore(reducer)
// actions
const increase = () => ({ type: INCREMENT })
const decrease = () => ({ type: DECREMENT })
// component
class _App extends React.Component {
render () {
const { number, increase, decrease } = this.props
return <div>
<button onClick={e => decrease()}>-</button>
<span>{number}</span>
<button onClick={e => increase()}>+</button>
</div>
}
}
const App = ReactRedux.connect(
state => ({ number: state.number }),
{ increase, decrease }
)(_App)
// render
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('container'))
Here is the latest source code. I paste the JavaScript code below:
/* global React, mobx, mobxReact, ReactDOM */
// store
const store = mobx.observable({
number: 0,
decrease () {
this.number -= 1
},
increase () {
this.number += 1
}
})
// component
class _App extends React.Component {
render () {
const store = this.props.store
return <div>
<button onClick={e => store.decrease()}>-</button>
<span>{store.number}</span>
<button onClick={e => store.increase()}>+</button>
</div>
}
}
const App = mobxReact.observer(_App)
// render
ReactDOM.render(<App store={store} />, document.getElementById('container'))
State Container | Lines of Code | new Concepts |
---|---|---|
SubX | 26 | None |
Redux | 43 | reducer , action , Provider , connect |
MobX | 27 | observable , observer |
Notes:
- Concepts shared by three approaches are omitted, such as
store
&component
- SubX does have the concept of
observable
&observer
. However it does NOT require developers to explicitly declare anything asobservable
orobserver
. SubX is smart enough to figure them out. That's why we say new concepts for SubX is "None". - I don't use decorator for MobX because decorator is not part of
@babel/preset-env
yet. And to be frank I don't know how to setup decorator support forbabel-standalone
(which compiles our code to ES5 in browser).
SubX has shorter code without sacrificing readability, no new concepts to learn or master.
TodoMVC is a project which offers the same Todo application implemented using MV* concepts in most of the popular JavaScript MV* frameworks of today.
Here is the source code for TodoMVC App implemented with SubX.
We won't paste the code here, please read it before continuing reading this article.
Here is the source code for TodoMVC App implemented with Redux.
We won't paste the code here, please read it before continuing reading this article.
Here is the source code for TodoMVC App implemented with MobX.
We won't paste the code here, please read it before continuing reading this article.
State Container | Lines of Code | new Concepts |
---|---|---|
SubX | 186 | autoRun , debounceTime |
Redux | 275 | selector , reducer , combineReducers , action , Provider , connect , store.dispatch , store.subscribe , _.debounce , |
MobX | 219 | observable , decorate , computed , action , autorun , observer |
Notes:
- Lines of Code were counted when I was writing this article. Latest code might have slightly fewer/more lines.
- Concepts shared by three approaches are omitted, such as
store
,component
&router
. - SubX does have similar concepts as MobX like
observable
,observer
&computed
. However it does NOT require developers to explicitly declare anything asobservable
,observer
orcomputed
. SubX is smart enough to figure them out. - I don't use decorator for MobX because decorator is not part of
@babel/preset-env
yet. And to be frank I don't know how to setup decorator support forbabel-standalone
(which compiles our code to ES5 in browser).
The requirement is: whenever store.todos
changes, save it to localStorage. To avoid saving too frequently, we want to debounce
the operation by 1000ms.
Saving todos to localStorages brings two new concepts: autoRun
& debounceTime
, and they are the only two new concepts that you need to master.
SubX.autoRun(store, () => {
window.localStorage.setItem('todomvc-subx-todos', JSON.stringify(store.todos))
}, debounceTime(1000))
SubX.autoRun
is a powerful and flexible method. The first argument store
is what we monitor, the second arument is the action we want to perform, the third argument (debounceTime
) is an RxJS operator.
So that every time store changes which might affect the result of the action, we performce the action again. And action performing is further controlled by the RxJS operators which are specified as third/fourth/fifth...arguments of SubX.autoRun
method.
Saving todos to localStorages brings two new concepts: store.subscribe
& _.debounce
let saveToLocalStorage = () => {
window.localStorage.setItem('todomvc-redux-todos', JSON.stringify(todosSelector(store.getState())))
}
saveToLocalStorage = _.debounce(saveToLocalStorage, 1000)
store.subscribe(() => saveToLocalStorage())
So that whenever store changes, we save todos to localStorage. Redux doesn't provide any utilities for debouncing, so we need debouce from Lodash.
We only want to save store.todos
, but store.subscribe
triggers even when we change store.visibility
.
More, if I execute store.dispatch(setTitle('id-of-todo', 'Hello world'))
100 times, store.subscribe
also triggers 100 times although the todo's title doesn't change for the last 99 times.
In order to fix this problem, we need to write more code, bring more new concepts to the solution. Thus, less developer-friendly.
Saving todos to localStorages brings one new concept: autorun
autorun(() => {
window.localStorage.setItem('todomvc-mobx-todos', JSON.stringify(store.todos))
}, { delay: 1000 }) // delay is throttle, don't know how to debounce using MobX
MobX's autorun
is similar to SubX's autoRun
. Except that it is not so flexible to allow user to specify RxJS operators to further control the action.
As you can see from the comment in the code snippet above, it doesn't meet our requirements yet. We want to deounce
, but what it does is throttle
.
I spent half an hour but failed to make debounce
work. I tried to use Lodash's debounce
to rescue but it seems that MobX's autorun
is incompatible with async actions.
In order to fix this problem, we need to write more code, bring more new concepts to the solution. Thus, less developer-friendly.
SubX has shorter code without sacrificing readability, fewer new concepts to learn or master.
I would like to conclude that: SubX is more developer-friendly than Redux & MobX.
Should you have different opinions, please leave comments! We welcome different opinions! Espcially those with concrete samples.
Images used in the article: