Skip to content

Instantly share code, notes, and snippets.

@sonhanguyen
Last active October 6, 2022 01:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sonhanguyen/6359773b2a5666e4aeff11a6573a6454 to your computer and use it in GitHub Desktop.
Save sonhanguyen/6359773b2a5666e4aeff11a6573a6454 to your computer and use it in GitHub Desktop.
use revealjs to view

MobX


  • What it is
  • Why do we need it
  • How do we use it
  • How it works


WHAT is Mobx


Reactive Programming

  • When something happen, do something else
    • Promise

How it is usually done

  • relying on some template engine (vuejs, angular)
    • not encoded in the actual code
  • callback (async.js, even-emitter, $.on, scope.$watch)
  • Rx.Observable
declare const rateStream, impressionStream: Observable<number>

const lineItemStream = Observable
  .combineLatest(rateStream, impressionStream)
  .map([rate, impression]) => ({
    cost: rate * inpression
  })
// ..
lineItemStream.subscribe(
  //...
)

// Very pervasive style

--

The gist of mobX

import { observable } from 'mobx'

class LineItem {
  @observable rate = 0.2
  @observable impression = 1
  
  get cost() {
    return this.rate * this.impression
  }
// ...
const lineItem = new LineItem

require('mobx').autorun(_ =>
  console.log("Cost change", lineItem.cost)
);

Transparent reactive programming

(function tick() {
  lineItem.impression++
  setTimeout(tick, 1000)
})()

// Cost change 0.4
// Cost change 0.6
// Cost change 0.8
// Cost change 1

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

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

The component does not care whether the props is "observable" or not

type LineIem = {
  props: {cost: any}
       | Observable<{cost: any}>
}

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 {
  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 =>
        <LineItem key={id} lineItem={lineItemsById.get(id)} />) // on-time binding
      }
    </ol>
)

--

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

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 Object.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)

Patterns


  • Modeling views
@observer
class EditableLineItem extends React.Component {
  impression = observable(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.lineItem.impression}
        onChange={({target}) => this.impression = target.value}
      />
      <button onClick={this.updateImpression}>Save</button>
    </div>
  }
}

can do completely away with setState

--

Props that are promises

class extends React.Component {
  constructor(props) {
    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 <div>
      {this.state.data}
    </div>
  }
}
import { lazyObservable } from 'mobx-utils'
class extends React.Component {
  data = lazyObservable(cb => this.props.fetch().then(cb))
  
  render() {
    return <div>
      {this.props.show && data.current()}
    </div>
  }
}

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(function tracked() {
  console.log(lineItem/**/.impression/**/, lineItem/**/.rate/**/)
})

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

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