Adding React to our stack

Backbone has been a game changer for SupportBee. However, time has made its limitations, when it comes to building views, very clear. We have a lot of code to hook data up to the views and we really wanted to get rid of it.

React has been on our radar since it launched and we recently added it to our stack. I am not going to discuss React since many have done that before much better than I could, check out this talk by Pete Hunt for example. Instead, this post will focus on how to make Coffeescript, Backbone.js and React play nicely.

Test drive your code

Yes, at SupportBee we test drive our code and we encourage you to do it too. Our examples will make use of Jasmine and Sinon.

Ready? Let’s start with a spec, I’ll call it ReactSpec.coffee.

describe "React dependencies", ->
  it "React should be defined", ->
    expect(React).toBeDefined()

  it "ReactDOM should be defined", ->
    expect(ReactDOM).toBeDefined()

  it "React addons should be defined", ->
    expect(React.addons).toBeDefined()

Run it and… watch it fail! How do we fix it?

Step 1: Rails Configuration

The React team provides a gem for using React in Rails. It also lets us use JSX and Coffeescript. Let’s add it to our Gemfile and run bundle install.

Note: Read carefully the documentation of the gem. Depending on the version you choose, you might need to download react files and put them into app/assets/javascript.

Run the test again. Does it fail?

Step 2: The Asset Pipeline

The asset pipeline requires you to add your assets in a manifest file, usually application.js. Now it should look like this

//= require jquery
//= require backbone
...
//= require react/react-with-addons
//= require react/react-dom

Run the test again. It passes! Congratulations! You have React set up.

Step 3: Create a React view

If you signup for a trial account in SupportBee you’ll see that, after you confirm your account, a modal will pop asking you to create an email address. This is how it looks.

Why don’t we recreate a bit of it? Start writing CreateEmailSpec.coffee.

describe 'CreateEmail', ->
  beforeEach ->
    @component = React.addons.TestUtils.renderIntoDocument Views.CreateEmail()

  afterEach ->
    ReactDOM.unmountComponentAtNode ReactDOM.findDOMNode(@component).parentNode

  it "should render with the correct DOM", ->
    renderedDOM = ReactDOM.findDOMNode(@component)

    expect(renderedDOM.tagName).toBe 'div'
    expect(renderedDOM.classList).toEqual(['container'])
    ...

Run it, watch it fail. Now let’s write our component CreateEmail.js.jsx.coffee.

CreateEmail = React.createClass(
  render: ->
    `<div className="container">
      <h2>Setup Your Email</h2>
      <p>You need to setup an email address and start forwarding your support
      emails to us.
      </p>
      <form>
        <input type="text" ref="email" />
        <input type="submit" value="Create Forwarding Address" />
      </form>
    </div>`
)

Views.CreateEmail = React.createFactory(CreateEmail)

Note: If you make use of JSX, don’t forget to name your React view files with the extension .js.jsx.coffee.

Meet Flux

React is great for views, but that’s it. Really. We won’t write an entire app with React alone. Backbone comes to the rescue. How do they relate? Facebook says “Lots of people use React as the V in MVC.” However they do not use the MVC architecture themeselves. Rather, Facebook introduced Flux. A detailed overview of this pattern is provided in its docs. You might also find interesting this cartoon guide. We’d love to hear your thoughts on Flux.

The short of it: Flux enforces a unidirectional data flow. Its major parts are the dispatcher, the stores and the views.

Let’s try it!

Step 4: Create a dispatcher

In Flux, every action is sent to all stores via the callbacks the stores register with the dispatcher. It has no logic. Your app should have a single dispatcher.

Facebook provides some utils including a dispatcher. Add it to our project the way we did with React before. Your ReactSpec.coffee should include this test

...
it "Flux should be defined", ->
  expect(Flux).toBeDefined()

Make sure it passes. Then create AppDispatcherSpec.coffee.

describe "AppDispatcher", ->
  it 'should be an instance of Flux.Dispatcher', ->
    expect(AppDispatcher instanceof Flux.Dispatcher).toBeTruthy()

Then AppDispatcher.coffee is as simple as this

SB.AppDispatcher = new Flux.Dispatcher

That’s it! We have a dispatcher.

Step 5: Create a store

A store registers itself with the dispatcher and provides it with a callback. Stores contain the application state and logic. Do you already have a place where you do that? Backbone models, perhaps? Right, Backbone models or collections can take the role of a Flux store! We just need to register a callback with the dispatcher.

In our use case, we want to add an email address. Given a Backbone model EmailAddress and a collection EmailAddresses, let us add a few lines to EmailAddressesSpec.coffee.

describe "EmailAddresses", ->
  ...
  it "should register a callback with the app dispatcher", ->
    expect(@collection.dispatchToken).not.toBe null

  it "should invoke registered callback on event", ->
    callback = sandbox.stub(@collection, 'dispatchCallback')
    payload =
      eventName: 'new-address'
      data: {}

    AppDispatcher.dispatch payload

    expect(callback).toHaveBeenCalledWith payload
    callback.restore()

To make it pass we need our EmailAddresses.coffee to look something like this

EmailAddresses = Backbone.Collection.extend
  initialize: ->
    @dispatchToken = AppDispatcher.register(@dispatchCallback)

  dispatchCallback: (payload) ->
    switch payload.eventName
      when 'new-address'
        # Add your new model accesible from payload.data
        # Backbone collections know how to post data

Step 6: Use your backbone store

Nice! You have a dispatcher and a store. All you need to now is make your form dispatch an event on submit.

Let’s get back to CreateEmailSpec.coffee and add some code to it.

it "should dispatch event on submit", ->
  spy = sandbox.spy(AppDispatcher, 'dispatch')
  renderedDOM = ReactDOM.findDOMNode(@component)

  React.addons.TestUtils.Simulate.submit(renderedDOM)

  expect(spy).toHaveBeenCalledOnce()
  spy.restore()

Once more, please. Run it and watch it fail. In order to make it pass our CreateEmail.js.jsx.coffee should look like this:

CreateEmail = React.createClass(
  submit: (e) ->
    AppDispatcher.dispatch
      eventName: 'new-address'
      data:
        email: @refs.email.getDOMNode().value

  render: ->
    `<div className="container">
      ...
      <form>
        <input type="text" ref="email" />
        <input type="submit" value="Create Forwarding Address" onClick={this.submit} />
      </form>
    </div>`
)

Views.CreateEmail = React.createFactory(CreateEmail)

Voila! We have a React view talking to a Backbone collection through a global dispatcher.

What’s next

For the sake of brevity and clarity I skipped form validation, view subscription to Backbone events and alike. But I hope this gives you a pretty good idea of how to merge Backbone, React, JSX and Coffescript together to make frontend development fun!

We are confident our renewed stack will help us to deliver enhanced user experience in the months ahead.



blog comments powered by Disqus
Josue Montano
Josue Montano