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 my implementation 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.resourcenUnfetched({ 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()
}
}