When the new guy starts writing in CoffeeScript

This is Morgan

yourgrocer.com.au

Rails app built on
Traditional web app with jQuery
Existing JSON API

Back to school

Facebook's React Tutorial
Learn React, Flux, and Flow: Part I - @colinmegill
React for stupid people - @andrewray
Flux for stupid people - @andrewray
Gmail

Flux in 18 lines

window.App =
  Components: {}
  Actions: {}
  Stores: {}
  
App.Dispatcher = MicroEvent.mixin
  register: (events)->
    for eventName, callback of events
      @bind eventName, callback
      
Actions =
  dispatch: (name, payload)->
    App.Dispatcher.trigger name, payload
    
App.createActions = (obj)->
  Object.assign(Object.create(Actions), obj)
  
Store =
  emitChange: -> @trigger 'change'
  
App.createStore = (obj)->
  store = Object.assign(Object.create(Store), obj)
  MicroEvent.mixin store
  

Components initiate Actions

{ CartActions } = App.Actions

App.Components.CartItem = React.createClass
  increment: ->
    CartActions.updateQuantity(@props.id, @props.quantity + 1)
    
  render: ->
    <a onClick={@increment}>
      <i className="icon icon-chevron-up" />
    </a>
    

Actions dispatch an event with a unique name and payload

App.Actions.CartActions = App.createActions
  updateQuantity: (id, quantity)->
    @dispatch 'cart-update-line-item-quantity', id, quantity
    

Actions are a good place to make requests

App.Actions.CartActions = App.createActions
  updateQuantity: (id, quantity)->
    @dispatch 'cart-update-line-item-quantity', id, quantity
    
    updateQuantity(id, quantity)
    
# each lineItem can have a timeout
timeouts = {}
updateQuantity = (id, quantity)->
  clearTimeout(timeouts[id]) if timeouts[id]?
  timeouts[id] = setTimeout ->
    SpreeAPI.updateQuantity id, quantity
  , 500
  

Stores

_order =
  lineItems: []
  
App.Stores.CartStore = CartStore = App.createStore
  getState: ->
    order: _order
    
updateQuantity = (id, quantity)->
  item = _order.lineItems.find (i)-> i.id == id
  item.quantity = quantity if item?
  
  CartStore.emitChange()
  
App.Dispatcher.register
  'cart-update-line-item-quantity': (id, quantity)->
    updateQuantity(id, quantity)
    

Components

{ CartStore } = App.Stores

App.Components.CartContainer = React.createClass
  getInitialState: ->
    CartStore.getState()
    
  componentDidMount: ->
    CartStore.bind 'change', @onChange
    
  componentWillUnmount: ->
    CartStore.unbind 'change', @onChange
    
  onChange: ->
    @setState CartStore.getState()
    

Container Components

{ CartStore } = App.Stores
{ Cart } = App.Components

App.Components.CartContainer = React.createClass
  getInitialState: ->
    CartStore.getState()
    
  componentDidMount: ->
    CartStore.bind 'change', @onChange
    
  componentWillUnmount: ->
    CartStore.unbind 'change', @onChange
    
  onChange: ->
    @setState CartStore.getState()
    
  render: ->
    # separate data fetching / rendering
    <Cart {...@state} />
    

So that's data

Computation

CartItem

{ currency } = App.Filters

App.Components.CartItem = React.createClass
  render: ->
    <div className={classNames('cart-item row', { 'cart-item-removed': @props.quantity == 0 })}>
      ...
      <div className="col">
        <QuantitySpinner quantity={@props.quantity} min=1 onIncrement={@increment} onDecrement={@decrement} />
      </div>
      <div className="col">
        <strong className="pull-right">{currency(@props.price * @props.quantity)}</strong>
      </div>
      <div className="col">
        <a className="btn btn-sm btn-danger pull-right" onClick={@remove}>
          <i className="icon icon-trash" />
        </a>
      </div>
    </div>
    

CartSummary

{ currency } = App.Filters

App.Components.CartSummary = React.createClass
  render: ->
    subTotal = _.sum @props.order.lineItems, (lineItem)->
      lineItem.price * lineItem.quantity
    gstTotal = @props.order.taxTotal
    deliveryTotal = @props.order.estimatedShipTotal
    adjustmentsTotal = _.sum @props.order.adjustments, (a)-> a.amount
    
    estimatedTotal = subTotal + deliveryTotal + adjustmentsTotal
    pendingClass = classNames('pending-update': @props.states.totalsPendingUpdate)
    
    <div className="cart-summary">
      <div className="total-item row">
        <div className="total-label">Groceries</div>
        <div className="total-value">{ currency(subTotal) }</div>
      </div>
      <div className={ "total-item row #{ pendingClass }"}>
        <div className="total-label">Delivery fee</div>
        <div className="total-value">{ currency(deliveryTotal) }</div>
      </div>
      { for adjustment in @props.order.adjustments
        <div className={ "total-item row #{ pendingClass }"}>
          <div className="total-label">{ adjustment.label }</div>
          <div className="total-value">{ currency(adjustment.amount) }</div>
        </div>
      }
      <div className={ "total-item row #{ pendingClass }"}>
        <div className="total-label">Estimated total</div>
        <div className="total-value">{ currency(estimatedTotal) }</div>
      </div>
    </div>
    

States on Stores

_order =
  lineItems: []
_states =
  pendingUpdate: false
  
App.Stores.CartStore = CartStore = App.createStore
  getState: ->
    order: _order
    states: _states
    
updateQuantity = (id, quantity)->
  item = _order.lineItems.find (i)-> i.id == id
  item.quantity = quantity if item?
  
  _states.pendingUpdate = true
  
  CartStore.emitChange()
  
updateTotals = (data)->
  _order = data.order
  _states.pendingUpdate = false
  
  CartStore.emitChange()
  
App.Dispatcher.register
  'cart-update-line-item-quantity': (id, quantity)-> updateQuantity(id, quantity)
  'order-totals-updated': (data)-> updateTotals(data)
  

CartSummaryContainer

Bootstrapping stores

#cart-component
  = react_component 'App.Components.CartContainer'
  
:coffee
  App.Stores.CartStore.bootstrap
    #{ render('orders/_order.jbuilder') }
App.Stores.CartStore = CartStore = App.createStore
  bootstrap: (data)->
    _order = data.order
    
    @emitChange()
    

Testing Filters

{ currency } = App.Filters

describe 'App.Filters', ->
  describe 'currency()', ->
    it 'should return empty string if NaN', ->
      expect(currency('boo')).to.equal('')
      
    it 'should return formatted dollars and cents', ->
      expect(currency(12.152)).to.equal('$12.15')
      expect(currency('12.152')).to.equal('$12.15')
      expect(currency(-12)).to.equal('-$12.00')
      expect(currency('-12')).to.equal('-$12.00')
      

Testing Actions / Stores

{ CartActions } = App.Actions
{ CartStore } = App.Stores

server = null
clock = null
order = null

describe 'CartActions', ->
  before ->
    server = sinon.fakeServer.create()
    clock = sinon.useFakeTimers()
    
  after ->
    server.restore()
    clock.restore()
    
  beforeEach ->
    order = chai.create('order')
    CartStore.bootstrap(order: order)
    
  describe 'incrementQuantity', ->
    it 'should increment total quantity in CartStore', ->
      totalQty = _.sum order.line_items, 'quantity'
      lineItem = order.line_items[0]
      CartActions.incrementQuantity(lineItem.id, lineItem.quantity + 1)
      
      newOrder = CartStore.getState().order
      newTotalQty = _.sum newOrder.line_items, 'quantity'
      
      expect(newTotalQty).to.equal(totalQty + 1)
      
  describe 'decrementQuantity', ->
    it 'should decrement quantity in CartStore', ->
      totalQty = _.sum order.line_items, 'quantity'
      lineItem = order.line_items[0]
      CartActions.decrementQuantity(lineItem.id, lineItem.quantity - 1)
      
      newOrder = CartStore.getState().order
      newTotalQty = _.sum newOrder.line_items, 'quantity'
      
      expect(newTotalQty).to.equal(totalQty - 1)
      
  describe 'removeLineItem', ->
    it 'should set quantity to 0 in CartStore', ->
      lineItem = order.line_items[0]
      CartActions.removeLineItem(lineItem.id)
      
      newOrder = CartStore.getState().order
      lineItem = newOrder.line_items.find (l)-> l.id == lineItem.id
      
      expect(lineItem.quantity).to.equal(0)
      
  describe 'applyCoupon', ->
    it 'should add an adjustment CartStore', ->
      CartActions.applyCoupon('IKNOWPETERPAN')
      # PUT /shop/api/orders/:id/apply_coupon_code
      
      server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify
        success: "The coupon code was successfully applied to your order."
        error: null
      server.requests.shift()
      
      # GET /shop/api/orders/:id
      order.adjustments.push
        eligible: true
        amount: '-4.00'
        label: 'Free delivery'
      server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify(order)
      server.requests.shift()
      
      newOrder = CartStore.getState().order
      
      expect(newOrder.adjustments.length).to.equal(order.adjustments.length)
      
    it 'should expose an error if the coupon is no good', ->
      CartActions.applyCoupon('IKNOWDARTH')
      
      server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify
        success: null
        error: "The coupon code was invalid."
      server.requests.shift()
      
      cartStates = CartStore.getState().states
      expect(cartStates.couponError).to.equal("The coupon code was invalid.")
      

Thoughts so far

Flux and React work well
React's tiny API is great
render() is messier than templates
There's an art to Components and Stores

Worth following

discuss.reactjs.org

@chantastic    @colinmegill

Thanks

@markbrown4