Search code examples
javascriptecmascript-6babeljsmobx

Difference between mobx's `action.bound` and arrow functions on class functions?


Using arrow functions on a class with babel transpiles it so the definition is bound in the constructor. And so it is not in the prototype and it is not available via super when inheriting. It is also not as efficient when scaling by creating many instances.

There are more blog posts on this topic, but I just wanted to know the difference in how mobx.action.bound is handled compared to arrow functions when using babel.

Comparing the two:

class Example {
   test = () => {
      console.log(this.message)
   }
}

class Example {
   @action.bound
   test() {
      console.log(this.message)
   }
}

Solution

  • There are 2 variables @action and @action.bound have an effect on:

    1. Binding: How this is bound in the resulting function.
    2. Prototype: If the resulting function is in the prototype.

    To summarize, these are the rules:

    • @action preserves the original function's binding and prototype-inclusion. If the original function is not bound, the result will not be, and vise versa. And if the original function is not in the prototype, the result will not be, and vise versa.
    • @action.bound will always result in a function which is bound, and which is in the prototype.

    How Binding is affected:

    You can easily test this like so:

    class Store {
      unbound() {
        console.log('unbound', this)
      }
    
      arrow = () => {
        console.log('arrow', this)
      }
    }
    const storeInstance = new Store()
    const unbound = storeInstance.unbound
    const arrow = storeInstance.arrow
    unbound()
    arrow()
    // console displays:
    // unbound undefined
    // arrow Store
    

    Now let's try adding @action:

    class Store {
      @action
      unbound() {
        console.log('unbound', this)
      }
    
      @action
      arrow = () => {
        console.log('arrow', this)
      }
    }
    const storeInstance = new Store()
    const unbound = storeInstance.unbound
    const arrow = storeInstance.arrow
    unbound()
    arrow()
    // console still displays:
    // unbound undefined
    // arrow Store
    

    Now let's try adding @action.bound:

    class Store {
      @action.bound
      unbound() {
        console.log('unbound', this)
      }
    
      @action.bound
      arrow = () => {
        console.log('arrow', this)
      }
    }
    const storeInstance = new Store()
    const unbound = storeInstance.unbound
    const arrow = storeInstance.arrow
    unbound()
    arrow()
    // console now displays:
    // unbound Store
    // arrow Store
    

    As you can see, @action maintains the function's bindings (or lack of binding). Meanwhile, @action.bound will always return a bound function, thus turning an unbound function into a bound one, and an already bound function will remain bounded.

    How prototype is affected:

    As for your concern about inheritance, here is the Store definition:

    class Store {
      unbound() {}
      arrow = () => {}
      @action unboundAction() {}
      @action.bound unboundActionBound() {}
      @action arrowAction = () => {}      
      @action.bound arrowActionBound = () => {}
    }
    

    And this is what the storeInstance looks like: enter image description here

    As you pointed out, arrow = () => {} is not part of the prototype. And to answer your question, @action arrow = () => {} will not result in a function which is in the prototype. It looks like @action preserves the previous behavior. However, @action.bound will always result in a function which is in the prototype.