Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rerendering and async workflow #5

Closed
emmenko opened this issue Dec 11, 2014 · 8 comments
Closed

Rerendering and async workflow #5

emmenko opened this issue Dec 11, 2014 · 8 comments

Comments

@emmenko
Copy link

emmenko commented Dec 11, 2014

Hi @jordangarcia

I'm currently learning React and the Flux architecture and came to your library when researching how to use Stores with Immutables. I've stumbled upon a couple of problems / questions though when applying it to my example, so let me explain a bit what I did.

In my example I want to fetch some data from an API when the component is rendered. In the ideal scenario with a Flux architecture it would look like this:

  • Component will render
  • Component asks state from Store
  • Store provides default state to component and triggers an Action to fetch data from an API
  • Component renders with initial state
  • Action creator execute async request using some util library
  • Action creator processes request and if successful will dispatch payload
  • Store handles dispatched payload and updates its (immutable) state
  • Store informs Component that it has a new state
  • Component rerender with new state

Now, when I implement this flow using the Reactor the last step is not executed, which could be the same problem @ArnoBuschmann is having #4

@emmenko
Copy link
Author

emmenko commented Dec 11, 2014

Damn it, I posted the comment without finishing it...will post the rest now

@emmenko
Copy link
Author

emmenko commented Dec 11, 2014

This is my setup

# orders/components/Orders.cjsx
React = require 'react'
{Getter} = require 'nuclear-js'
NuclearReactMixin = require 'nuclear-react-mixin'
reactor = require '../../Reactor'
OrdersStore = require '../OrdersStore'

reactor.attachStore 'ordersStore', OrdersStore, false #silent

module.exports = React.createClass

  mixins: [NuclearReactMixin(reactor)]

  componentDidMount: ->
    OrdersStore.getState()

  getDataBindings: ->
    orders: Getter 'ordersStore.orders' # uses KeyPath
    total: Getter 'ordersStore', (o) -> o.get('total')

  render: ->
    <div className='orders'>
      <h1>There are {@state.total} orders</h1>
      <ul>
        {@state.orders.map (order) ->
          <li>{order.get('id')}: {order.get('total')}</li>
        .toJS()}
      </ul>
    </div>
# orders/OrdersStore.coffee
{Immutable, Store} = require 'nuclear-js'
{Map, List} = Immutable
OrdersActions = require './OrdersActions'

module.exports = Store

  getInitialState: ->
    Map
      orders: List()
      total: 0
      count: 0
      offset: 0

  initialize: ->
    # subscribe to dispatches
    @on 'ordersFetched', (state, payload) ->
      state.mergeDeep
        orders: payload.results
        total: payload.total
        count: payload.count
        offset: payload.offset

  getState: ->
    # let's get some real data
    OrdersActions.fetchOrders()
# orders/OrdersActions.coffee
reactor = require '../Reactor'
WS = require '../shared/libs/WS'

module.exports =
  fetchOrders: (opts = {}) ->
    WS.get '/api/orders'
    .then (result) ->
      reactor.dispatch 'ordersFetched', result
    .fail (e) ->
      console.error e
      # TODO: we can dispatch for an error
# WS.coffee
$ = require 'jquery'

module.exports =
  get: (url) ->
    $.ajax
      url: url
      dataType: 'json'

@emmenko
Copy link
Author

emmenko commented Dec 11, 2014

So here are my questions:

  • how would you set it up when requesting data from a server?
  • how would you update the immutable state when new data is coming?
  • also I saw that when transforming with map on @state.orders, I have to transform it back with .toJS() otherwise I get the Immutable object (you can see what I mean in this fiddle in the console logs - http://jsfiddle.net/emmenko/kpczpzjm/)

Any idea / help would be much appreciated :)

@madebyherzblut
Copy link

Did you figure it out @emmenko? I am currently trying to implement async workflow as well. @jordangarcia do you have any best practices?

@jordangarcia
Copy link
Contributor

Here are some of the patterns & best practices Optimizely's used in its Flux implementation:

  • We have actionCreators that execute an asynchronous request and hook into the deferred thenable which dispatches an action onto the system. We've built up quite a system around fetching API data and using flux / nuclear as a frontend data store / caching layer.

An simplified example of this workflow:

/**
 * Dispatches action when an API fetch request starts
 * @private
 *
 * @param {string} entity
 * @param {Deferred} deferred
 */
function onFetchStart(entity, deferred) {
  flux.dispatch(apiActionTypes.ENTITY_FETCH_START, {
    entity: entity,
    deferred: deferred,
  })
}

/**
 * Dispatch an ENTITY_FETCH_SUCCESS event to the system
 * @private
 *
 * @param {string} entity
 * @param {object|array} response
 */
function onFetchSuccess(entity, response) {
  flux.dispatch(apiActionTypes.ENTITY_FETCH_SUCCESS, {
    entity: entity,
    data: response,
  })
  return response
}

/**
 * Dispatch an ENTITY_FETCH_FAIL event to the system
 * @private
 *
 * @param {string} entity
 * @param {string} reason
 */
function onFetchFail(entity, reason) {
  flux.dispatch(apiActionTypes.ENTITY_FETCH_FAIL, {
    entity: requestInfo.get('entity'),
    reason: reason,
  })
  return reason
}

function fetchAll(filters) {
  var entity = 'projects'
  var onSuccess = onFetchSuccess.bind(null, entity)
  var onFail = onFetchFail.bind(null, entity)
  var url = generateApiUrl(entity, filters)
  var deferred = $.ajax(url).then(onSuccess, onFail)
  onFetchStart.call(null, entity, deferred)
  return deferred
}

There is a store that listens for these api actionTypes and it looks like

var Nuclear = require('../../nuclear')
var apiActionTypes = require('../constants/api-action-types')

var toImmutable = Nuclear.toImmutable

module.exports = Nuclear.Store({
  getInitialState() {
    return {}
  },

  initialize() {
    this.on(apiActionTypes.ENTITY_FETCH_SUCCESS, loadEntityData)
    this.on(apiActionTypes.ENTITY_PERSIST_SUCCESS, loadEntityData)
    this.on(apiActionTypes.ENTITY_DELETE_SUCCESS, onDeleteSuccess)
  }
})

/**
 * payload.data
 * payload.entity
 * @param {Immutable.Map} state
 * @param {object} payload
 */
function loadEntityData(state, payload) {
  var entity = payload.entity
  var data = payload.data

  if (!_.isArray(data)) {
    data = [data]
  }

  return state.withMutations(state => {
    data.forEach(entry => {
      state.setIn([entity, entry.id], toImmutable(entry))
    })
  })
}

/**
 * payload.id
 * payload.entity
 * @param {Immutable.Map} state
 * @param {object} payload
 */
function onDeleteSuccess(state, payload) {
  var entity = payload.entity
  var id = payload.id

  return state.removeIn([entity, id])
}

This store when populated looks like:

// Immutable.Map
{
  projects: {
    1: { id: 1, description: 'project 1', ... },
    2: { id: 1, description: 'project 2', ... },
    3: { id: 1, description: 'project 3', ... },
  },
  users: {
    1: { id: 1, username: 'user1', ... },
    2: { id: 1, username: 'user2', ... },
  },
}

All data bindings in the UI are between components and the store (named 'modelCache')

React.createClass({
  mixins: [NuclearReactMixin(reactor)],

  getDataBindings() {
    return {
      projects: 'modelCache.projects',
    }
  },
})

By programming this reactively using Getters and getDataBindings() then most async actions become fire and forget. In our actionCreators we do return a Promise that is resolved when the async request is completed but that is more for knowing when the action is done, the resolved value is almost never used.

The main drawback to this style is knowing when specific requests are done, we've solved this by creating a loadingStore that hooks into ajax requests and allows us to assign a string key to a specific request then we can show in the UI whether something is loaded or not.

In VueJS it looks like this

ui.loadingWhen('main-section', ajaxDeferred)
<div v-loading="main-section">...</div>

This v-loading directive observes the reactor and creating an overlay and loading spinner over the div whenever the main-section key is loading.

Hope this helps! It's quite a different paradigm of programming asynchronous workflows, but having a reactive functional mindset and composing Getters together makes it very powerful and scales quite well.

@madebyherzblut
Copy link

Wow thanks for the detailed answer @jordangarcia!

@emmenko
Copy link
Author

emmenko commented Jan 8, 2015

@jordangarcia thanks for the explanation :)

@tomconroy
Copy link

The question about using .toJS(), this is fixed in React 0.13 facebook/react#2376

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants