Skip to content

Instantly share code, notes, and snippets.

@adamcee
Last active March 14, 2018 20:47
Show Gist options
  • Save adamcee/3191762f2af43ec62a4b335b66955cc8 to your computer and use it in GitHub Desktop.
Save adamcee/3191762f2af43ec62a4b335b66955cc8 to your computer and use it in GitHub Desktop.
Example of code to generate reducers and actions for "generic" or basic HTTP requests to GET/POST JSON data.

Example of code to generate reducers and actions for "generic" or basic HTTP requests to GET/POST JSON data. The idea is that each particular request (for user account info, for X most recent comments, etc) has its own reducer which is only responsible for tracking the state of that HTTP request and holding response data. With redux-thunk, callbacks to handle success/error states (i.e., do something with data, or, display error message to the user) can then be programmatically executed once the request has completed.

In general, I have found it useful to have reducers be small and responsible for one specific thing, and using redux-thunk to have action creator functions access data and pass it to other reducers (via actions, of course) when necessary. Thus there will be a large number of very small reducers.

This increases re-usability of code between projects (especially since everything is in a single file because of the redux ducks pattern, and uses a common set of helper/utility functions), and makes it easier to change the application as relationships between reducers are isolated to the functions used as thunks in the action creators (hope that was not written poorly).

This is an example of a reducer, action creator functions, and action types (all in a single file using the redux ducks pattern . It also imports helper functions for async requests. I've added documentation for the one used, but its a pretty nontrivial set of functions for async calls, and should probably be described in another gist. Look below in this gist for source of the "genericRequest" creator functions.

'use strict';
/***
 * permissions_request.js
 * Uses redux-ducks pattern for reducers, action types, and action creator functions.
 */
// npm modules
import Immutable from 'immutable';

// app modules
import { setRequestError, setRequestSuccessJson,
    makeRequestWithSuccessAndError } from 'actions/async';
import { getRequestReduction, makeGenericRequestActionTypes,
    makeGenericRequestReducer } from 'reductions/request';
import { getHost, createDefaultHeader } from 'utils';
import { setPermissions } from 'redux_ducks/permissions';

/*********
 * Types
 * ******/
export const TYPES = makeGenericRequestActionTypes("PERMISSIONS");

/*********
 * Actions
 * ******/
export function getPermissionsRequest(){
    const headers = createDefaultHeader();
    return makeRequestWithSuccessAndError(
        getHost() + 'my_app/v1/permissions',
        {method: 'GET',  headers: headers},
        handleSuccess,
        TYPES.success,
        TYPES.error,
        handleError,
        TYPES.requesting);
}

function handleSuccess(dispatch, json, startEvent, data){
    dispatch(setRequestSuccessJson(data.successAction, {payload: json}));
    dispatch(setPermissions(json));
}

function handleError(dispatch, json, startEvent, data){
    if(json.json && json.json.permissions){
        dispatch(setPermissions(json.json.permissions));
    }
}
/*********
 * Reducer
 * ******/
const initialState = Immutable.Map({
    request: getRequestReduction("Error requesting permissions."),
});

export const reducer = makeGenericRequestReducer(TYPES, initialState);

Here is the documentation for the makeRequestWithSuccessAndError function used.

/***
 * makeRequestWithSuccessAndError
 * Wrapper for makeJsonRequest, with an error callback function.
 *
 * Make an asynch request, and then execute a success callback
 * in addition to a success action, and also execute an error callback on error..
 *
 * See makeRequestWithSuccess for details.
 *
 * @param {string} endpoint URL to make request at
 * @param {object} reqInfo Object containing request infromation. Ex:
 *      `{method: 'GET', headers: new Headers({"X-Auth-Token": token })}`
 *
 * @param {function} successCallback function to be executed on request success
 * @param {function} additionalErrorCallback function to be executed on request error, in addition to the `handleError` function
 * @param {constant} successAction action to dispatch on success
 * @param {constant} errorAction action to dispatch if request fails
 * @param {constant} waitingAction action to dispatch while request in progress
 */

Code to create reducers, action types, and actions for "generic" http requests (basically getting/sending JSON).

// redux_http_request_helpers.js
'use strict';
import Immutable from 'immutable';
/**
 * All of these functions work together.
 * They provide a way to consistently create an immutable map
 * with a consistent data structure to represent the state of
 * a particular request, and then ways to consistently modify
 * particular keys of that request state as needed.
 *
 * The goal is to reduce the amount of work needed to implement http request flow
 * for common/generic/basic http request and response-handling within our redux/react app.
 *
 * a mix of this approach and the previous approach.
 */

/***
 * Generate the most common set of action types
 * for a generic http request flow reducer
 * @param {string} baseActionName The unique part of the action type
 *                 which will be used. Should always be caps. For example,
 *                 "UPDATE_PROFILE" will generate:
 *                      "UPDATE_PROFILE_REQUEST"
 *                      "SET_REQUESTING_UPDATE_PROFILE"
 *                      "UPDATE_PROFILE_REQ_SUCCESS"
 *                      "UPDATE_PROFILE_REQ_ERROR"
 * @return {Immutable.Record} instance of an Immutable.js RequestActionTypes Record
 * */
export function makeGenericRequestActionTypes(baseTypeName){
    return new RequestActionTypes({
        request: `${baseTypeName}_REQUEST`,
        requesting: `SET_REQUESTING_${baseTypeName}`,
        success: `${baseTypeName}_SUCCESS`,
        error: `${baseTypeName}_ERROR`,
    });
}

// Record for generic request Action Types
// Should be used via makeGenericRequestActionTypes()
const RequestActionTypes = Immutable.Record({
    request: null,
    requesting: null,
    success: null,
    error: null,
});

/**
 * Generate the most common reducer for a generic http request
 * @param {Immutable.Record} actionTypesRecord action types
 * @param {Immutable.Map} initialState initial state for the state this reducer handles
 *                        This must always contain a map generated by getRequestReduction()
 *                        which is set to the key "request".
 * @return {func} reducer function
 */
export function makeGenericRequestReducer(actionTypesRecord, initialState){
    return function reducer(state = initialState, action) {
        switch (action.type) {
            case actionTypesRecord.requesting:
                return updateIsMakingRequest(state, "request", action);
            case actionTypesRecord.success:
                return updateRequestState(state, "request", action);
            case actionTypesRecord.error:
                return updateRequestState(state, "request", action);
            default:
                return state;
        }
    };
}


/***
 * Utilities / Helpers
 */


/**
 * Helper function to create a consistent data structure for tracking the state
 * of a request within a reducer.
 *
 * @param {string} errorMessage Error message to display if request fails
 * @return Immutable.Map Map represetning the state of a request
 */
export function getRequestReduction(errorMessage){
    return Immutable.Map({
        isMakingRequest: false,
        isError: false,
        respCode: null,
        respMsg: errorMessage,
        respBody: null,
    });
}

/**
 * Helper function to update the "isMakingRequest" property
 * Use when a request has been sent
 * @param {object} state Redux state
 * @param {string} key key for the request whose state (immutable map) we wish to update
 * @param {object} action the action associated with the request, its payload we will store in "isMakingRequest"
 * @return {object} Updated state where myKey.isMakingRequest is now updated with the payload of the action
 */
export function updateIsMakingRequest(state, key, action){
    return state.update(key, (former) => former.update("isMakingRequest", (last) => action.payload))
}

export function updateRespCode(state, key, action){
    return state.update(key, (former) => former.update("respCode", (last) => action.payload))
}

/**
 * Helper function to completely update the Immutable.Map
 * which represents the state of a request and its response.
 * Mainly for updating the parts dealing with the response data.
 *
 * @param {Immutable.Map} state Redux state
 * @param {string} key key for the request whose state (immutable map) we wish to update
 * @param {object} action the action associated with the request.
 * @return {object} Updated state.
 *
 * NOTE: "isMakingRequest" gets updated somewhere else in the flow for async
 * requests. Attempting to update it here will produce errors.
 */
export function updateRequestState(state, key, action){
    let newState = updateIsError(state, key, action);
    newState = updateRespMsg(newState, key, action);
    newState = updateRespCode(newState, key, action);
    newState = updateRespBody(newState, key, action);
    return newState;
}


export function getTempValueRequestReduction(errorMessage, defaultValue){
    return Immutable.Map({
        isMakingRequest: false, isError: false, respCode: null,
        respMsg: errorMessage, tempValue: defaultValue, value: defaultValue,
    })
}

/**
 * Helper function to update the "isError" property
 * Use when a request has failed and an error has been returned
 * @param {object} state Redux state
 * @param {string} key key for the request whose state (immutable map) we wish to update
 * @param {object} action the action associated with the request, its payload we will store in "isError"
 * @return {object} Updated state where myKey.isError is now updated with the payload of the action
 *                  Said payload will probably contain an error message from the server.
 */
export function updateIsError(state, key, action){
    return state.update(key, (former) => former.update("isError", (last) => action.error))
}

export function updateRespMsg(state, key, action){
    if(!action.json || !action.json.message){
        return state;
    }
    return state.update(key, (former) => former.update("respMsg", (last) => action.json.message))
}

export function updateRespBody(state, key, action){
    if(!action.json){
        return state;
    }
    return state.update(key, (former) => former.update("respBody", (last) => Immutable.fromJS(action.json)))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment