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()
}
}