Last active
August 29, 2015 14:27
-
-
Save kyldvs/a9cb6e4da12750f7b2ff to your computer and use it in GitHub Desktop.
An approach to async data fetching using flux/utils
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* One approach to async data fetching using Flux utils. | |
*/ | |
// A helper class to deal with an asynchronous process, this is very basic, there | |
// is a lot of room for improvement here, like memoizing async tokens where data | |
// is null | |
type AsyncState = 'not_loaded' | 'loading' | 'error' | 'loaded'; | |
class AsyncToken<T> extends Immutable.Record({state: null, data: null}) { | |
state: AsyncState; | |
data: ?T; | |
constructor(state: AsyncState, data: ?T) { | |
super({state, data}); | |
} | |
setState(state: AsyncState): AsyncToken<T> { | |
return this.set('state', state); | |
} | |
setData(data: ?T): AsyncToken<T> { | |
return this.set('data', data); | |
} | |
} | |
// Cache this since it is the default return value | |
var NOT_LOADED = new AsyncToken('not_loaded', null); | |
// Now create a base store that helps us move through this process | |
class FluxAsyncMapStore<K, V> extends FluxReduceStore<Immutable.Map<K, AsyncToken<V>>> { | |
_startLoadAction: string; | |
constructor(dispatcher: Dispatcher) { | |
super(dispatcher); | |
// Make sure this is unique per store | |
this._startLoadAction = 'start-load-' + this.getDispatchToken(); | |
// Hack so that subclasses do not always have to call super.reduce() when | |
// overriding the reduce function | |
var actualReduce = this.reduce.bind(this); | |
this.reduce = (state, action) => { | |
if (action.type === this._startLoadAction) { | |
state = this._updateLoading(state, action.keys); | |
} | |
return actualReduce(state, action); | |
}; | |
} | |
getInitialState(): State { | |
return Immutable.Map(); | |
} | |
/** | |
* When you request data that is not loaded this will cause a load to happen as a | |
* side effect. Note that this method in particular could be pulled out so that | |
* the request happens outside of the store in an action creator, this is just a | |
* more convenient way of requesting data. | |
*/ | |
get(key: K): AsyncToken<V> { | |
var state = this.getState(); | |
var shouldLoad = !state.has(key) || state.get(key).getState() === 'not_loaded'; | |
if (shouldLoad) { | |
// It's important to queue these up and dedupe them rather than fire | |
// an async request for each, that aspect of this implementation is left | |
// out for the sake of brevity | |
setTimeout(() => this._doStartLoad(key), 0); | |
setTimeout(() => this.__load(key), 0); | |
} | |
// Make sure we always return an Async token | |
return state.get(key) || NOT_LOADED; | |
} | |
/** | |
* This is the standard getter that will not cause side effects fetches. | |
*/ | |
getCached(key: K): AsyncToken<V> { | |
return this.getState().get(key) || NOT_LOADED; | |
} | |
/** | |
* This is an abstract method that every subclass must override. It's where you | |
* can do some async process in order to load your data. | |
* | |
* Note that there is no return value, the data MUST re-enter the store through | |
* an action being fired. Within the context of this method it's safe to fire a | |
* new action because it's called within a setTimeout(). | |
*/ | |
__load(key: K): void { | |
invariant(false, 'implement this'); | |
} | |
/** | |
* Fires the loading action. | |
*/ | |
_doStartLoad(key: K): void { | |
this.getDispatcher().dispatch({ | |
type: this._startLoadAction, | |
// Keeping a queue of keys and updating them all at once is left as an | |
// exercise for the reader :) | |
keys: [key], | |
}); | |
} | |
/** | |
* Update all of the given keys to the loading state. | |
*/ | |
_updateLoading(state: State, keys: Iterable<K>): State { | |
return state.withMutations(mutableMap => { | |
for (var key of keys) { | |
if (!mutableMap.has(key)) { | |
mutableMap.set(key, new AsyncToken('loading', null)); | |
} else { | |
mutableMap.set(key, mutableMap.get(key).setState('loading')); | |
} | |
} | |
}); | |
} | |
} | |
// An implementation of the store | |
class PostStore extends FluxAsyncMapStore<string, Object> { | |
reduce(state: State, action: Action): State { | |
switch (action.type) { | |
case 'post/handle-load': | |
return state.set(action.id, new AsyncToken('loaded', action.post)); | |
case 'post/handle-error': | |
return state.set(action.id, new AsyncToken('error', action.error)); | |
default: | |
return state; | |
} | |
} | |
__load(key: string): void { | |
MyAPIUtils.promiseLoadPost(key).then( | |
(response) => dispatch({type: 'post/handle-load', id: key, post: response}), | |
(error) => dispath({type: 'post/handle-error', id: key, error}), | |
); | |
} | |
} | |
var MyAPIUtils = { | |
promiseLoadPost(key: string): Promise<Object> { | |
// Implementation omitted | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment