Skip to content

Instantly share code, notes, and snippets.

@tylerlong
Last active January 19, 2022 16:05
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tylerlong/a5d7d179fb75415e9971f9a720f5c907 to your computer and use it in GitHub Desktop.
Why is SubX more developer-friendly than Redux & MobX?

Why is SubX more developer-friendly than Redux & MobX?

First of all, what is SubX?

SubX, Subject X, Reactive Subject. Pronunciation: [Sub X]

SubX is next generation state container. It could replace Redux and MobX in our React apps.

Features of SubX

  • 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

Secondly, let's define what is developer-friendly?

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.

  1. The fewer lines of code, the better.
  2. The fewer new concepts, the better.

The fewer lines of code, 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:

  1. No line could be longer than 120 characters.
  2. Code must be readable by human beings.

The fewer new concepts, the better

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.

A Counter App

It is a simple app. It's so simple that we only use it as appetizer.

image

Counter App implemented with SubX

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'))

Counter App implemented with Redux

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'))

Counter App implemented with MobX

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'))

Summurize

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 as observable or observer. 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 for babel-standalone(which compiles our code to ES5 in browser).

SubX has shorter code without sacrificing readability, no new concepts to learn or master.

Conclusion: In this simple Counter App competition, SubX is the clear winner!

A TodoMVC App

TodoMVC is a project which offers the same Todo application implemented using MV* concepts in most of the popular JavaScript MV* frameworks of today.

image

TodoMVC App implemented with SubX

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.

TodoMVC App implemented with Redux

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.

TodoMVC App implemented with MobX

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.

Summurize

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 as observable, observer or computed. 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 for babel-standalone(which compiles our code to ES5 in browser).

About saving todos to localStorage

The requirement is: whenever store.todos changes, save it to localStorage. To avoid saving too frequently, we want to debounce the operation by 1000ms.

How SubX save todos to localStorage

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.

How Redux save todos to localStorage

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.

❗️Problem with Redux's approach

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.

How MobX save todos to localStorage

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.

❗️Problem with MobX's approach

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.

In this TodoMVC App competition, SubX is again the winner!

Final conclusion

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.

@zxdong262
Copy link

It is true, even I can see its power.

@zxdong262
Copy link

zxdong262 commented Oct 16, 2018

Since rxjs is not required to use subx, this way might be more friendly, at least for users who do not understand rxjs yet(like me), do not need to learn autorun/debounceTime or other rxjs operator.

store.$.subscribe(
  _.debounce(
    event => {
      if (event.path.includes('todos')) {
        global.localStorage.setItem('todomvc-subx-todos', JSON.stringify(store.todos))
      }
    },
    100
  )
)

@tylerlong
Copy link
Author

tylerlong commented Oct 17, 2018

@zxdong262

Yes, I agree. For people who has no experience with RxJS at all, it's indeed a challenge to learn and master RxJS operators.

I would like to adjust your code a little:

const debouncedSave = _.debounce(() =>{
 global.localStorage.setItem('todomvc-subx-todos', JSON.stringify(store.todos))
}, 1000)
store.$.subscribe(event => {
  if(event.path[0] === 'todos') {
    debouncedSave()
  }
})

With above being said, I ALWAYS recommend you to learn RxJS. It's far more powerful & flexible than those async utilities provided by Lodash. Once you mastered RxJS, you will find the following way more natural:

store.$.pipe(
  filter(event => event.path[0] === 'todos'),
  debounceTime(1000)
).subscribe(event => {
  global.localStorage.setItem('todomvc-subx-todos', JSON.stringify(store.todos))
})

Think of RxJS as Lodash for events.

Quoted from https://github.com/ReactiveX/rxjs/blob/master/doc/introduction.md

@tylerlong
Copy link
Author

@zxdong262

autoRun is provided by SubX, it is not a RxJS operator. For end developers, this is the simplest way:

SubX.autoRun(store, () => {
  window.localStorage.setItem('todomvc-subx-todos', JSON.stringify(store.todos))
}, debounceTime(1000))

autoRun is smart to figure out which events to monitor, so you don't need to tell it filter(event => event.path[0] === 'todos')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment