-
-
Save thomasboyt/7b4a64e3b7346d41d2a7 to your computer and use it in GitHub Desktop.
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
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); | |
} | |
}; | |
} |
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
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; | |
} | |
}; | |
} |
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
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)); | |
} | |
}), | |
}) |
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
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(), | |
}) | |
} | |
} | |
} |
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
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