Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Last active August 29, 2015 14:17
Show Gist options
  • Save mattmccray/1d511ba1ee3affd01b7e to your computer and use it in GitHub Desktop.
Save mattmccray/1d511ba1ee3affd01b7e to your computer and use it in GitHub Desktop.
Example async data fetching pattern.

Async Data Fetching

Consider it a given that this talks to a RESTful API (simple crud and, in my case, predicate filtering).

There is a FetchStore that manages the API calls, ensuring there aren't duplicate calls, resolving promises once data arrives, etc. The store itself is rather opaque. It doesn't have any public accessors.

The FetchActions defines two actions for clients to call, and two for other stores to consume in the dispatch cycle:

import alt from './alt'

class FetchActions {

  constructor() {
    this.generateActions(
      // Stores listen for this, payload contains resource type,
      // query, and response data.
      'resourceFetched',
      // Same, except contains error information
      'resourceFetchFailure'
    )
  }

  // A 'fire and forget' method used by other stores. Just
  // signals that data needs to be fetched for this type
  // and query (usually an id).
  loadResource( type, query) {
    this.dispatch({ type, query })
  }

  // Primarily used in resolving data pre-route rendering,
  // this returns a Promise that resolves once the resource(s)
  // have been loaded and processed through the dispatcher.
  retrieveResource( type, query) {
    return new Promise(( resolve, reject) => {
      function callback( err, data) {
        if( err) reject( err)
        else resolve( data)
      }
      this.dispatch({ type, query, callback })
    })
  }

}

export default alt.createActions( FetchActions)

That's basically it. Internally, the FetchStore keeps track of pending requests, and any associated callbacks, resolving them once the xhr returns.

Here's an example of how it's used by other Stores:

import alt from './alt'
import CompanyActions from './CompanyActions'
import FetchActions from './FetchActions'

const COMPANY_TYPE= "Company"

class CompanyStore {

  constructor() {
    this.bindActions( CompanyActions)
    this.bindActions( FetchActions)

    this.idmap= {}
  }

  onResourceFetched({ type, query, data }) {
    if( type === COMPANY_TYPE) {
      this.idmap[ data.companyId]= data
    }
    else {
      return false
    }
  }

  static get( id) {
    const company= this.getState().idmap[ id]

    if( Type.isUndefined( company)) {
      FetchActions.loadResource( COMPANY_TYPE, id)
    }

    return company
  }

}

export default alt.createStore( CompanyStore, 'CompanyStore')

In the application, I treat undefined and null differently. If a value is undefined it's unfetched data, if null then it's been fetched with no results


Just for completeness, here's the implementation of my FetchStore. It's not plug-n-play because I have a class (Resource) that wraps the CRUD url generation and xhr calling. But it'd be easy to hook up with raw xhr calls.

import alt from './alt'
import FetchActions from './FetchActions'
import {Resource} from 'toolkit'

class FetchStore {

  constructor() {
    this.bindActions( FetchActions)

    this.queue= {}
  }

  onLoadResource({ type, query }) {
    this._loadResource( type, query)
  }

  onRetrieveResource({ type, query, callback }) {
    this._loadResource( type, query, callback)
  }

  _loadResource( type, query, callback) {
    const token= this._tokenize( type, query),
          enqueued= this.queue[ token],
          api= Resource.type( type)

    if( enqueued) {
      // Request already sent...
      if( callback) {
        // Add this callback to the other queued callbacks
        enqueued.callbacks.push( callback)
      }
      return
    }

    this.queue[ token]= defaultState()

    const apiCall= (Type.isObject( query) ? api.find( query) : api.get( query))
      .then( this._resourceResponse.bind( this, type, query, token, true))
      .catch( this._resourceResponse.bind( this, type, query, token, false))
  }

  _resourceResponse( type, query, token, success, data) {
    const info= this.queue[ token]

    if( success) {
      FetchActions.resourceFetched({ type, query, token, data})
    }
    else {
      FetchActions.resourceFetchFailure({ type, query, token, data})
    }

    info.callbacks
      .forEach( callback => {
        if( success) callback( null, data)
        else callback( data)
      })

    delete this.queue[ token]
  }

  _tokenize( type, query) {
    return JSON.stringify({ type, query})
  }
}

export default alt.createStore( FetchStore, 'FetchStore')

function defaultState() {
  return {
    callbacks: [],
    error: null,
    finish: null,
    start: new Date()
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment