Last active
November 30, 2016 19:55
-
-
Save thomasboyt/ca8edefb0ef07b8ea9077f50d5464575 to your computer and use it in GitHub Desktop.
Revisions
-
thomasboyt revised this gist
Nov 30, 2016 . 1 changed file with 11 additions and 11 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -29,8 +29,8 @@ import {getRequest} from 'LIB_NAME'; function mapStateToProps(state, props) { return { // gets the request with the unique key actionTypes.getUser -> props.userId request: getRequest(state, [actionTypes.getUser, props.userId]); }; } ``` @@ -44,18 +44,18 @@ import {requestStart, requestError, requestSuccess} from 'LIB_NAME'; export function fetchUser(userId) { return async (dispatch) => { dispatch(requestStart([actionTypes.fetchUser, userId])); const resp = await window.fetch(/* ... */); if (resp.status !== 200) { const err = await resp.json(); dispatch(requestError([actionTypes.fetchUser, userId], error)); return; } dispatch(requestSuccess([actionTypes.fetchUser, userId])); const data = await resp.json(); @@ -75,7 +75,7 @@ To reset a request - for example, to ensure that when you exit and return to a p ```js import {requestReset} from 'LIB_NAME'; dispatch(requestReset([actionTypes.getUser, userId])); ``` _TODO: can an in-flight request be reset?_ @@ -120,7 +120,7 @@ function mapStateToProps(state, props) { const {id} = props; return { request: getRequest(state, [actionTypes.fetchUser, id]); user: state.users[id], }; } @@ -135,18 +135,18 @@ import {requestStart, requestError, requestSuccess} from 'LIB_NAME'; export function fetchUser(userId) { return async (dispatch) => { dispatch(requestStart([actionTypes.fetchUser, userId])); const resp = await window.fetch(/* ... */); if (resp.status !== 200) { const err = await resp.json(); dispatch(requestError([actionTypes.fetchUser, userId], error)); return; } dispatch(requestSuccess([actionTypes.fetchUser, userId])); const data = await resp.json(); @@ -189,7 +189,7 @@ You can create a counter in your action creator: let todoRequestId = 0; export function createTodo(text) { return async (dispatch) => { dispatch(requestStart([actionTypes.createTodo, todoRequestId])); // ... }; } -
thomasboyt revised this gist
Nov 30, 2016 . 2 changed files with 145 additions and 0 deletions.There are no files selected for viewing
File renamed without changes.This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,145 @@ import _ from 'lodash'; import updeep from 'updeep'; function createReducer(initialState, handlers) { return (state = initialState, action) => { if (handlers.hasOwnProperty(action.type)) { return handlers[action.type](state, action); } else { return state; } }; } // Action constants (intended for internal use only) const REQUEST_ACTION_START = '@request/start'; const REQUEST_ACTION_SUCCESS = '@request/success'; const REQUEST_ACTION_ERROR = '@request/error'; const REQUEST_ACTION_RESET = '@request/reset'; // Request status constants export const REQUEST_IDLE = 'idle'; export const REQUEST_PENDING = 'pending'; export const REQUEST_SUCCESS = 'success'; export const REQUEST_ERROR = 'error'; /** * Request shape: * - status: one of REQUEST_IDLE, REQUEST_PENDING, REQUEST_SUCCESS, REQUEST_ERROR * - error: an error value * - key: the unique key of the request */ const initialState = {}; export const reducer = createReducer(initialState, { [REQUEST_ACTION_START]: (state, {key}) => { const req = getRequest(state, key); if (req.status === REQUEST_PENDING) { // TODO: Have a better bail-out path for this? // Could be moved to the action creator and dispatch e.g. REQUEST_ACTION_ATTEMPTED_RESTART_PENDING // So the frontend could handle by ignoring/canceling/etc? throw new Error(`Attempted to restart pending action ${key}`); } return updateRequest(state, key, { status: REQUEST_PENDING, }); }, [REQUEST_ACTION_SUCCESS]: (state, {key}) => { return updateRequest(state, key, { status: REQUEST_SUCCESS, }); }, [REQUEST_ACTION_ERROR]: (state, {key, error}) => { return updateRequest(state, key, { status: REQUEST_ERROR, error, }); }, [REQUEST_ACTION_RESET]: (state, {key}) => { return updateRequest(state, key, { status: REQUEST_IDLE, }); }, }); function updateRequest(state, key, opts) { return updeep.updateIn(key, updeep.constant(createRequest(key, opts)), state); } function createRequest(key, opts) { return { key, ...opts, }; } // Selectors export function getRequest(state, key) { ensureArray(key); const req = _.get(state, key); if (!req) { return createRequest(key, {status: REQUEST_IDLE}); } return req; } // Actions export function requestStart(key) { ensureArray(key); return { type: REQUEST_ACTION_START, key, }; } export function requestError(key, error) { ensureArray(key); return { type: REQUEST_ACTION_ERROR, key, error, }; } export function requestSuccess(key) { ensureArray(key); return { type: REQUEST_ACTION_SUCCESS, key, }; } export function requestReset(key) { ensureArray(key); return { type: REQUEST_ACTION_RESET, key, }; } function ensureArray(key) { if (!Array.isArray(key)) { throw new Error(`Invalid request key ${key}: should be an array`) } } -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -6,13 +6,13 @@ Managing async action state in Redux is a very, very common topic of discussion. The classical way to do it is to create three action types (for start, error, and success), and then create boilerplate to set loading/error states for each action inside a reducer. This boilerplate adds up fast if you're creating an app with lots of async actions! This library abstracts away this boilerplate with a :sparkles: magic :sparkles: `requests` reducer that will handle storing and retrieving this state across your application. ## Usage ### Using Request State A "request" is simply a tiny little state machine that lives in a `requests` reducer. It's keyed off of a name (usually an action type) and, optionally, a unique key to help track multiple requests of the same type. By keeping a request in your Redux state, you can easily trigger a request (through an action creator) in a component and react to the loading/error states of that request in a different component. It is also easy to prevent multiple, identical requests from being in-flight at once. -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -179,7 +179,7 @@ export default function userReducer(state = {}, action) { ### Can my own reducers handle the `request*` action creators? They potentially could, but a better way to handle custom state updating would be to simply dispatch another action alongside your `request*` action. ### How can I have multiple instances of a request if there is no obvious unique key to use? -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -6,7 +6,7 @@ Managing async action state in Redux is a very, very common topic of discussion. The classical way to do it is to create three action types (for start, error, and success), and then create boilerplate to set loading/error states for each action inside a reducer. This boilerplate adds up fast if you're creating an app with lots of async actions! This library abstracts away this boilerplate with a :sparkles: magic :sparkles: `request` reducer that will handle storing and retrieving this state across your application. ## Usage -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,6 +1,6 @@ # redux "request" idea (this is adapted from the [redux-happy-async](https://github.com/thomasboyt/redux-happy-async) project I made earlier this year, but I think simplified in an easier-to-understand way) ## Why? -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -6,7 +6,7 @@ Managing async action state in Redux is a very, very common topic of discussion. The classical way to do it is to create three action types (for start, error, and success), and then create boilerplate to set loading/error states for each action inside a reducer. This boilerplate adds up fast if you're creating an app with lots of async actions! This library abstracts away this boilerplate with a :sparkles: magic :sparkles: reducer that will handle storing and retrieving this state across your application. ## Usage -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 6 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -2,6 +2,12 @@ (this is adapted from the [redux-happy-async](https://github.com/thomasboyt/redux-happy-async) project I made earlier this year) ## Why? Managing async action state in Redux is a very, very common topic of discussion. The classical way to do it is to create three action types (for start, error, and success), and then create boilerplate to set loading/error states for each action inside a reducer. This boilerplate adds up fast if you're creating an app with lots of async actions! This library abstracts away this boilerplate with a :sparkles: magic :sparkles: reducer ## Usage ### Using Request State -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -154,10 +154,11 @@ export function fetchUser(userId) { ```js // reducers/users.js export default function userReducer(state = {}, action) { if (action.type === actionTypes.fetchUser) { const {id} = action.user; return { [id]: action.user, ...state, -
thomasboyt revised this gist
Nov 28, 2016 . 1 changed file with 105 additions and 9 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -2,6 +2,8 @@ (this is adapted from the [redux-happy-async](https://github.com/thomasboyt/redux-happy-async) project I made earlier this year) ## Usage ### Using Request State A "request" is simply a tiny little state machine that lives in a `request` reducer. It's keyed off of a name (usually an action type) and, optionally, a unique key to help track multiple requests of the same type. @@ -34,25 +36,25 @@ Requests are created in action creators: ```js import {requestStart, requestError, requestSuccess} from 'LIB_NAME'; export function fetchUser(userId) { return async (dispatch) => { dispatch(requestStart(actionTypes.fetchUser, userId)); const resp = await window.fetch(/* ... */); if (resp.status !== 200) { const err = await resp.json(); dispatch(requestError(actionTypes.fetchUser, userId, error)); return; } dispatch(requestSuccess(actionTypes.fetchUser, userId)); const data = await resp.json(); dispatch({ type: actionTypes.fetchUser, user: data, }); }; @@ -72,13 +74,107 @@ dispatch(requestReset(actionTypes.getUser, userId)); _TODO: can an in-flight request be reset?_ ## Full Example ```js // components/UserDisplay.jsx import React from 'react'; import {connect} from 'react-redux'; import {getRequest, REQUEST_ERROR, REQUEST_SUCCESS} from 'LIB_NAME'; import fetchUser from '../actions/fetchUser'; import * as actionTypes from '../actionTypes'; class DisplayUser extends React.Component { componentWillMount() { const {id, dispatch} = this.props; dispatch(fetchUser(id)); } render() { const {request, user} = this.props; if (request.status === REQUEST_SUCCESS) { return ( <span>Username: {user}</span>; ); } else if (request.status === REQUEST_ERROR) { return ( <span>Error fetching user: {request.error}</span> ); } return <span>Loading user...</span>; } } function mapStateToProps(state, props) { const {id} = props; return { request: getRequest(state, actionTypes.fetchUser, id); user: state.users[id], }; } export default connect(mapStateToProps)(UserDisplay); ``` ```js // actions/fetchUser.js import {requestStart, requestError, requestSuccess} from 'LIB_NAME'; export function fetchUser(userId) { return async (dispatch) => { dispatch(requestStart(actionTypes.fetchUser, userId)); const resp = await window.fetch(/* ... */); if (resp.status !== 200) { const err = await resp.json(); dispatch(requestError(actionTypes.fetchUser, userId, error)); return; } dispatch(requestSuccess(actionTypes.fetchUser, userId)); const data = await resp.json(); dispatch({ type: actionTypes.fetchUser, user: data, }); }; } ``` ```js // reducers/users.js export default function userReducer(state = {}, action) { if (action.type === actionTypes.fetchUser) { const {id} = action.user; return { [id]: action.user, ...state, }; } return state; } ``` ## FAQ ### Can my own reducers handle the `request*` action creators? They potentially could, but a better way to handle custom state updating would be to simply dispatch another action if an error is encountered. ### How can I have multiple instances of a request if there is no obvious unique key to use? You can create a counter in your action creator: @@ -94,7 +190,7 @@ export function createTodo(text) { You'll likely want to then get a list of pending requests to be able to reference/display these requests (see below). ### How can I manage pending requests of a given type? You can filter for requests from the async reducer: -
thomasboyt created this gist
Nov 28, 2016 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,104 @@ # redux "request" idea (this is adapted from the [redux-happy-async](https://github.com/thomasboyt/redux-happy-async) project I made earlier this year) ### Using Request State A "request" is simply a tiny little state machine that lives in a `request` reducer. It's keyed off of a name (usually an action type) and, optionally, a unique key to help track multiple requests of the same type. By keeping a request in your Redux state, you can easily trigger a request (through an action creator) in a component and react to the loading/error states of that request in a different component. It is also easy to prevent multiple, identical requests from being in-flight at once. The request holds the following state: - `status`: This is either `REQUEST_IDLE`, `REQUEST_PENDING` `REQUEST_SUCCESS`, or `REQUEST_ERROR`. - `error`: This is an error value supplied by the action creator when the error state is entered. - `key`: This is the unique key of the request, if supplied. Within a component, you can fetch a request from the state: ```js import {getRequest} from 'LIB_NAME'; function mapStateToProps(state, props) { return { // gets the request for actionTypes.getUser with the unique key props.userId request: getRequest(state, actionTypes.getUser, props.userId); }; } ``` ### Creating Requests Requests are created in action creators: ```js import {requestStart, requestError, requestSuccess} from 'LIB_NAME'; export function getUser(userId) { return async (dispatch) => { dispatch(requestStart(actionTypes.getUser, userId)); const resp = await window.fetch(/* ... */); if (resp.status !== 200) { const err = await resp.json(); dispatch(requestError(actionTypes.getUser, userId, error)); return; } dispatch(requestSuccess(actionTypes.getUser, userId)); const data = await resp.json(); dispatch({ type: actionTypes.getUser, user: data, }); }; } ``` If you attempt to start an already-started request, an error will be thrown. ### Resetting Requests To reset a request - for example, to ensure that when you exit and return to a page, you do not see state from a past request - simply use: ```js import {requestReset} from 'LIB_NAME'; dispatch(requestReset(actionTypes.getUser, userId)); ``` _TODO: can an in-flight request be reset?_ ### FAQ #### Can my own reducers handle the `request*` action creators? They potentially could, but a better way to handle custom state updating would be to simply dispatch another action if an error is encountered. #### How can I have multiple instances of a request if there is no obvious unique key to use? You can create a counter in your action creator: ```js let todoRequestId = 0; export function createTodo(text) { return async (dispatch) => { dispatch(requestStart(actionTypes.createTodo, todoRequestId)); // ... }; } ``` You'll likely want to then get a list of pending requests to be able to reference/display these requests (see below). #### How can I manage pending requests of a given type? You can filter for requests from the async reducer: ```js const createTodoRequests = store.getState().requests[actionTypes.createTodo]; const pending = createTodoRequests.map((request) => request.status === REQUEST_PENDING); ```