Skip to content

Instantly share code, notes, and snippets.

@thomasboyt
Last active November 30, 2016 19:55
Show Gist options
  • Select an option

  • Save thomasboyt/ca8edefb0ef07b8ea9077f50d5464575 to your computer and use it in GitHub Desktop.

Select an option

Save thomasboyt/ca8edefb0ef07b8ea9077f50d5464575 to your computer and use it in GitHub Desktop.

Revisions

  1. thomasboyt revised this gist Nov 30, 2016. 1 changed file with 11 additions and 11 deletions.
    22 changes: 11 additions & 11 deletions 00-readme.md
    Original 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 for actionTypes.getUser with the unique key props.userId
    request: getRequest(state, actionTypes.getUser, props.userId);
    // 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));
    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));
    dispatch(requestError([actionTypes.fetchUser, userId], error));
    return;
    }

    dispatch(requestSuccess(actionTypes.fetchUser, userId));
    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));
    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);
    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));
    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));
    dispatch(requestError([actionTypes.fetchUser, userId], error));
    return;
    }

    dispatch(requestSuccess(actionTypes.fetchUser, userId));
    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));
    dispatch(requestStart([actionTypes.createTodo, todoRequestId]));
    // ...
    };
    }
  2. thomasboyt revised this gist Nov 30, 2016. 2 changed files with 145 additions and 0 deletions.
    File renamed without changes.
    145 changes: 145 additions & 0 deletions 01-code.js
    Original 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`)
    }
    }
  3. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions redux-request.md
    Original 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: `request` reducer that will handle storing and retrieving this state across your application.
    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 `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.
    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.

  4. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion redux-request.md
    Original 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 if an error is encountered.
    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?

  5. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion redux-request.md
    Original 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.
    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

  6. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion redux-request.md
    Original 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)
    (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?

  7. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion redux-request.md
    Original 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
    This library abstracts away this boilerplate with a :sparkles: magic :sparkles: reducer that will handle storing and retrieving this state across your application.

    ## Usage

  8. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions redux-request.md
    Original 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
  9. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion redux-request.md
    Original 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,
  10. thomasboyt revised this gist Nov 28, 2016. 1 changed file with 105 additions and 9 deletions.
    114 changes: 105 additions & 9 deletions redux-request.md
    Original 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 getUser(userId) {
    export function fetchUser(userId) {
    return async (dispatch) => {
    dispatch(requestStart(actionTypes.getUser, userId));
    dispatch(requestStart(actionTypes.fetchUser, userId));

    const resp = await window.fetch(/* ... */);

    if (resp.status !== 200) {
    const err = await resp.json();

    dispatch(requestError(actionTypes.getUser, userId, error));
    dispatch(requestError(actionTypes.fetchUser, userId, error));
    return;
    }

    dispatch(requestSuccess(actionTypes.getUser, userId));
    dispatch(requestSuccess(actionTypes.fetchUser, userId));

    const data = await resp.json();

    dispatch({
    type: actionTypes.getUser,
    type: actionTypes.fetchUser,
    user: data,
    });
    };
    @@ -72,13 +74,107 @@ dispatch(requestReset(actionTypes.getUser, userId));

    _TODO: can an in-flight request be reset?_

    ### FAQ
    ## 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?
    ### 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?
    ### 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?
    ### How can I manage pending requests of a given type?

    You can filter for requests from the async reducer:

  11. thomasboyt created this gist Nov 28, 2016.
    104 changes: 104 additions & 0 deletions redux-request.md
    Original 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);
    ```