Skip to content

Instantly share code, notes, and snippets.

@kyldvs
Last active August 29, 2015 14:27
Show Gist options
  • Save kyldvs/a9cb6e4da12750f7b2ff to your computer and use it in GitHub Desktop.
Save kyldvs/a9cb6e4da12750f7b2ff to your computer and use it in GitHub Desktop.
An approach to async data fetching using flux/utils
/**
* 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