Skip to content

Instantly share code, notes, and snippets.

@sonhanguyen
Created Nov 16, 2017
Embed
What would you like to do?
use revealjs to view

MobX


"How I learn to love React" cliche

  • What it is
  • Why do we need it
  • How do we use it
  • Some gotchas
  • Demo

WHAT is Mobx

--

Reactive Programming

How reactive programming is usually done - relying on some template engine (vuejs, angular) - not encoded in the actual code - callback (`async.js`, `even-emitter`, `$.on`, scope.$watch) - `.then`, `await` - `Rx.Observable` ```ts const lineItemStream = Observable .combineLatest(rateStream, impressionStream) .map([rate, impression]) => ({ rate, impression, cost: rate * inpression }) // .. lineItemStream.subscribe( //... ) // Very pervasive style ```

--

The gist of mobX

class LineItem {
  @observable rate = 0.2
  @observable impression = 1
  
  @computed get cost() {
    return this.rate * this.impression
  }
// ...
const lineItem = new LineItem
require('mobx').autorun(_ =>
  console.log("Cost change", lineItem.cost)
);
(function tick() {
  lineItem.impression++
  setTimeout(tick, 1000)
})()

--

How it works

  • Intercepting access to properties
$ node
> const { observable } = require('mobx')
> observable({
>   impression: 1,
>   rate: 0.2,
> })

​​​​​{ impression: [Getter/Setter], rate: [Getter/Setter] }​​​​​
  • Tracking data dependencies
autorun(_ =>
  console.log(lineItem/**/.impression/**/, lineItem/**/.rate/**/)
)
  • Transparent reactive programming

mobx-react
```ts import {observer} from 'mobx-react'

@observer class LineItem extends React.Component { render() { return cost: {this.props.cost} } } // or as function const LineItem = observer(({cost}) => cost: {cost} )

<small>The component does not care whether the props is "observable" or not</small>
```ts
type LineIem = {
  props: {cost: any}
       | Observable<{cost: any}>
}

--

More on observevables

const name = observable('Product name')
const product = observable({
  name: 'Product name',
  publisherId: 01383,
})

product.name = 'new name' // for existing property
extendObservable(object, patch) // instead of _.extends

const hashMap = observable(new Map) // instead of plain objects
for (const key of hashMap) { // ES6 iterable compliant
//...
const products = observable( [ ] )
_.map(products, console.info)

Why

  • Front-end data store
  • Composability in Redux
    • combineReducer
    • createSelector (from another library)
    • import {selectors, actions} from '../a/subdirectory'
  • A lot of name to be given that are just kind of the same
  • Code is verbose, yet obscure

What do we use it for?

  • Modeling domain stores
class LineItemStore {
  lineItemsById = observable.map()
}

class Api {
  updateLineItem(patch: LineItem): Promise<LineItem>
}
class CampaignService implements Api {
  constructor(
    public lineItemStore: LineItemStore
    private lineItemApi: Api
  ) { }

  async updateLineItem(patch) {
    const lineItem = await this.lineItemApi.updateLineItem(patch)

    return this.lineItemStore.lineItemsById.set(lineItem.id, lineItem)
  }
}

--

  • @observer
import { observer } from 'mobx-react'
const Campaign = obsever(
  ({ campaignService: { lineItemStore } }) =>
    <ol>
      {lineItemStore.lineItemsById.keys().map(id =>
        <EditableLineItem key={id} lineItem={lineItemsById.get(id)} />) // on-time binding
      }
    </ol>
)

--

  • Dependency injection
<Provider
    campaignService={new CampaignService(// ...
  <App />
</Provider>
// can have multiple Provider
@inject('campaignService')
// @inject({ campaignService, telemetryService } => { ...someProps })
@observe
class Campaign {

Patterns


  • Modeling views
@observer
class EditableLineItem extends React.Component {
  @observable impression = null
  // viewModel = new LineItem

  updateImpression = () => {
    const lineItemStore = this.props
    lineItemStore.updateLineItem({ id: this.props.id, impression: this.impression })
    this.impression = null;
  }
  render() {
    return <div>
      <input
        value={this.impression || this.lineItem.impression}
        onChange={({target}) => this.impression = target.value}
      />
      <button onClick={this.updateImpression}>Save</button>
    </div>
  }
}

can do completely away with setState

--

Local modifications

import { createViewModel } from 'mobx-utils'

@observer
class EditableLineItem extends React.Component {
  // will proxy all access to the original observable
  // as long as there is no local modification
  editableLineItem = createViewModel(this.props.lineItem)

  updateImpression = () => {
    this.props.lineItemStore.updateLineItem(
      this.editableLineItem/**/.localValues/**/
    )
  }
  render() {
    return <div>
      <input
        value={this.editableLineItem.impression}
        onChange={({target}) => {
          this.editableLineItem.impression = Number(target.value)
        }}
      />
      <button onClick={this.updateImpression}>Save</button>
      <button onClick={() => this.editableLineItem/**/.reset()/**/}>
        Cancel{/*After reset will switch back to proxy mode*/}
      </button>
    </div>
  }
}

--

Props that are promises

class extends React.Component {
  constructor(props) { super(...arguments)
    props.publisherApiPromise.then(
      publishersById => this.setState({ publishersById })
    )
  }
  render() {
    return this.state.publishersById ? 'loaded': 'loading..'
  }
}
import { fromPromise } from 'mobx-utils'
class extends React.Component {
  publishersById = fromPromise(this.props.publisherApiPromise)
  
  render() {
    return this.publishersById.value ? 'loaded': 'loading..'
  }
}

--

Lazy loading

class extends React.Component {
  componentDidMount() {
    this.props.fetch().then(data => this.setState(data))
  }
  render() {
    return <Modal show={this.props.show}>
      {this.state.data}
    </Modal>
  }
}
import { lazyObservable } from 'mobx-utils'
class extends React.Component {
  data = lazyObservable(cb => this.props.fetch().then(cb))
  
  render() {
    return <Modal show={props.show}>
      {this.props.show && data.current()}
    </Modal>
  }
}

Pros & cons


  • Pros
    • Does not impose a coding style on the code base
    • Fits well with familiar patterns (business objects, facades...)
    • Fit well with unidirectional data flow
  • Cons
    • Implicit
    • Inconsistencies
      obervable.prop
    
      obsevable.get()
    • Dereferencing (more like a gotcha)
      function Cost({ cost }) { return cost }
      function Cost({ lineItem }) { return lineItem.cost }
    • Immutability

--

More api stuff

import { PropTypes } from 'mobx-react'

  • observableArray
  • observableArrayOf(React.PropTypes.number)
  • observableMap
  • observableObject
  • arrayOrObservableArray
  • arrayOrObservableArrayOf(React.PropTypes.number)
  • objectOrObservableObject

--

Optimizations

  • @observable.shallow
  • @observable.deep
  • @observable.ref
  • @action
  • @transaction
  • @computed
    • @computed.struct
  • createTransformer
  • mobx.toJS

Where to go from here


Bonus

import 'ng-mobx'
 
angular.module('app', [
  //...
  'ng-mobx',
  //...
])
import {store} from './store/counter'
 
angular.component('myComponent', {
  controller: () => this.store = store,
  controllerAs: '$ctrl',
  template: `
    <div mobx-autorun>
      {{ $ctrl.store.value }} - {{ $ctrl.store.computedValue }}
      <button ng-click="$ctrl.store.action()">Action</button>
    </div>
  `
})

Recap

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