Skip to content

Instantly share code, notes, and snippets.

@thomasboyt
Last active December 30, 2015 01:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasboyt/7b4a64e3b7346d41d2a7 to your computer and use it in GitHub Desktop.
Save thomasboyt/7b4a64e3b7346d41d2a7 to your computer and use it in GitHub Desktop.
function noop(action, state) {
return state;
}
/*
* name - name of state field to set async state in
* startType, successType, errorType - action types, e.g. FOO_START, FOO_SUCCESS, FOO_ERROR
* onStart, onSuccess, onError (all optional) - called with (action, state) after async state is set
*
* uniqueKey (optional) - used to maintain multiple states under the same namespace.
* so, e.g. if you had 5 todos that had a "complete" action that need to have separately tracked state, you would set uniqueKey to something like `todoId`, and then pass that as a key in your action payload. then the cancel todo's async action state would be available under ['cancelTodoState', todoId, 'loading|error']
* without this, it's assumed only one instance of this async action is going at once
*/
export default function asyncReducer({name, startType, successType, errorType, onStart, onSuccess, onError, uniqueKey}) {
onStart = onStart || noop;
onSuccess = onSuccess || noop;
onError = onError || noop;
function getKeyPath(action) {
if (uniqueKey) {
const id = action[uniqueKey];
if (id === undefined) {
throw new Error(`Could not get unique id ${uniqueKey} for async reducer for ${action.type}, did you pass it?`);
}
return [name, id];
}
return [name];
}
return {
[startType]: function(action, state) {
const keyPath = getKeyPath(action);
const newState = state
.setIn([...keyPath, 'loading'], true)
.setIn([...keyPath, 'error'], null);
return onStart(action, newState);
},
[errorType]: function(action, state) {
const keyPath = getKeyPath(action);
const newState = state
.setIn([...keyPath, 'loading'], false)
.setIn([...keyPath, 'error'], action.error);
return onError(action, newState);
},
[successType]: function(action, state) {
const keyPath = getKeyPath(action);
const newState = state
.setIn([...keyPath, 'loading'], false)
.setIn([...keyPath, 'error'], null);
return onSuccess(action, newState);
}
};
}
export default function createImmutableReducer(initialState, handlers) {
return (state = initialState, action) => {
if (handlers[action.type]) {
const newState = handlers[action.type](action, state);
return newState;
} else {
return state;
}
};
}
const initialState = {
todos: I.List([]),
addTodoState: I.Map(),
completeTodoState: I.Map(),
};
const todosReducer = createImmutableReducer(initialState, {
...createAsyncReducer({
name: 'addTodoState',
startType: ADD_TODO_START,
successType: ADD_TODO_SUCCESS,
errorType: ADD_TODO_ERROR,
onSuccess: ({todo}, state) => {
return state.todos.push(I.fromJS(todo));
}
}),
...createAsyncReducer({
name: 'completeTodoState',
startType: COMPLETE_TODO_START,
successType: COMPLETE_TODO_SUCCESS,
errorType: COMPLETE_TODO_ERROR,
uniqueKey: 'todoId',
onSuccess: ({resp}, state) => {
// [there might be a cleaner way to do this in immutable-js, but for a real project i'd probably use I.Map here anyway]
const todoIdx = state.get('todos').findIndex((todo) => todo.id === resp.get('id'));
return state.updateIn(['todos', todoIdx], (todo) => todo.set('complete', true));
}
}),
})
export function completeTodo(todo) {
const todoId = todo.get('id');
return async function({dispatch}) {
dispatch({
type: COMPLETE_TODO_START,
todoId,
});
const resp = await window.fetch('/api/todos/complete', {method: 'POST'});
if (resp.status !== 200) {
dispatch({
type: COMPLETE_TODO_ERROR,
todoId,
error: await resp.text(),
})
} else {
dispatch({
type: COMPLETE_TODO_SUCCESS,
todoId,
resp: await resp.json(),
})
}
}
}
function select(state) {
return {
completeTodoState: state.todos.completeTodoState
};
}
const Todo = React.createClass({
handleComplete() {
this.props.dispatch
},
renderAction() {
const completeTodoState = this.props.completeTodoState.get(this.props.todo.id);
if (completeTodoState.get('loading')) {
return <LoadingSpinner />;
} else if (completeTodoState.get('error')) {
return <ErrorIcon />;
} else {
return (
<a onClick={this.props.dispatch(completeTodo())}>
<CheckIcon />
</a>
);
}
},
render() {
return (
<li>
{this.renderAction()}
{todo.text}
</li>
)
}
});
export default connect(select)(Todo);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment