Neolithic Prancing Minotaurs

    redux-request-tracker

    1.0.5 • Public • Published

    redux-request-tracker

    CircleCI Coverage Status

    About redux-request-tracker

    What is redux-request-tracker?

    redux-request-tracker is a package that aims to make working with async data and pagination (link headers support only) a breeze.

    Why redux-request-tracker?

    So there are number of other packages / patterns out there for tracking async state in your redux applications. here is a good artical for how redux recommends tacking async state. Note that the state is mixed in alongside the data it is related to.

    postsBySubreddit: {
        frontend: {
            isFetching: true,
            didInvalidate: false,
            items: []
        },
        reactjs: {
            isFetching: false,
            didInvalidate: false,
            lastUpdated: 1439478405547,
            items: [ 42, 100 ]
        }
    }

    This is one vaild approach for handling async data, but it comes with tradeoffs.

    1. Boilerplate: There is a decent amount of boilerplate that needs to go into your actions and reducers to handle all of the async logic, INVALIDATE_SOMETHING, REQUEST_SOMETHING, RECEIVE_SOMETHING.
    2. Mixed-in metadata: With this approach each reducer is responsible for updating & tracking it's own async data.

    redux-request-tracker takes a different approach. All async actions pass through middleware which is responsible for setting up all the request, response, and failure logic. Then using higher order view components and data accessors, you're able to tap into request state managed by the middleware. There are some nice side effects that come along with organizing your code like this.

    1. All async logic is separated for raw data Okay so what does that really mean? Let's say you have a posts collection. This collection no longer needs to care about whether or not it's loading so it can just focus on the state of posts. When your UI needs to respond to async events, like isLoading, you query your request store using the provided data accessors & or use the provided HO component(s).
    2. All async data is collocated What does that buy you? Glad you asked :) so now you can easily perform queries on all you async state to determine;
      • "how often do I make this request?",
      • "what routes call which endpoints",
      • "how often does this request get called?",
      • and so on.
    3. Request state history I just alluded to this a bit in the previous point, but the way redux-request-tracker stores its data, it accumulates requests in working memory for all async events that have been triggered for the life of the session. This makes it easy to answer some of the questions above.

    Getting Started

    Install

    npm install -S redux-request-tracker

    Setup

    There are essentially two required steps to integrate redux-request-tracker into your redux backed application.

    Step One (add middleware)

    To add redux-request-tracker middleware to your project, import the middleware module and combine it with your other redux middleware.

    NOTE: redux-request-tracker does have a dependency on redux-thunk to work properly.

    import { createStore, applyMiddleware } from 'redux'
    import thunkMiddleware from 'redux-thunk'                               // <- IMPORT THUNK
    import createLogger from 'redux-logger'
    import rootReducer from './reducers'
    import { middleware as requestMiddleware } from 'redux-request-tracker' // <- IMPORT MIDDLEWARE
     
    const loggerMiddleware = createLogger()
     
    export default function configureStore(preloadedState) {
      return createStore(
        rootReducer,
        preloadedState,
        applyMiddleware(
          thunkMiddleware,    // <- ADD THUNK MIDDLEWARE
          loggerMiddleware,
          requestMiddleware() // <- ADD MIDDLEWARE
        )
      )
    }

    Step Two (add reducer)

    To add the redux-request-tracker reducer to your project, import the reducer module and combine it with your other reducers.

    import { combineReducers } from 'redux'
    import todos from './todos'
    import counter from './counter'
    import { reducer as request } from 'redux-request-tracker' // <- IMPORT REDUCER
     
    export default combineReducers({
      todos,
      counter,
      request // <- ADD REDUCER | NOTE! As of right now, you must name this reducer "request".
    })

    Using redux-request-tracker

    After adding the reducer and middleware you're all set and ready to begin using redux-request-tracker. All that is required now is to write / modify your actions so they get picked up by your request tracking middleware.

    export const getTodos = () => {
      return (dispatch) => {
        dispatch({
          type: 'REQUEST_GET_TODOS',
          request: fetch('/api/todos'),
          onSuccess: (todos) => {
            dispatch({
              type: 'TODOS_UPDATE_TODOS',
              payload: todos
            });
          },
          onFailure: (err) => {
              // Respond to error
          },
          complete: () => {
              // Do something on error or success
          }
        });
      };
    };

    In this example were using the fetch api, but as long as the dispatched action matches the redux-request-tracker middleware signature, you can use any xhr library.

    Middleware signature

    For your requests to be picked up by the middleware layer, all you need to do is attach a promise to the request property.

    The following are the currently supported request action options.

    • type: String (required) unique namespace for the action
    • request: Promise (required)
    • onSuccess: Function (optional) callback that receives the request body
    • onFailure: Function (optional) callback that receives the response error on failure
    • complete: Function (optional) callback that is triggered after success or failure

    Middleware options

    The redux-request-tracker middleware takes the following options

    • onUnauthorized: Function (request) => any | optional | callback triggered on all 401 responses
    • getRequestMethod: Function (request) => string | optional | override for getting the request method from your request object, may differ from request lib to request lib.
    • getRequestUrl: Function (request) => string | optional | override for getting the request url from your request object, may differ from request lib to request lib.

    Connecting to your components

    As a convience, redux-request-tracker exposes connectRequest, a higher order function that binds request metadata to your components.

    connectRequest

    Connect request can take the following parameters in an options hash

    • requestName string | function (props) => string
    import React, { Component } from 'react';
    import { connectRequest } from 'redux-request-tracker';
     
    class MyTodoComponent extends Component {
        render() {
            return // ...
        }
    }
     
    export default connectRequest({ requestName: 'REQUEST_GET_TODOS' })(MyTodoComponent);
    Dynamic request tracking

    When connecting a component to your request store, you may not have a hard coded action that maps to a specific request. Lets say, for example, that you have a component that lists out a collection of TODOS. You could create the constant REQUEST_FETCH_TODOS in your actions that you would pass to the list views connectRequest function. Now lets say you have a component that only displays a single request and you have an action responsible for fetching individual todos. In this case you'll need a way to dynamically namespace your requests for each todo. To accomplish this, connectRequest, allows you to define the requestName option as a function that receives props as an argument. The following example uses the params prop passed in from react-router to get the id out of the path and append it to the requestName.

    import React, { Component } from 'react';
    import { connectRequest } from 'redux-request-tracker';
     
    const REQUEST_GET_TODO = (id) => `REQUEST_GET_TODO_${id}`;
     
    class MyTodoComponent extends Component {
        render() {
            return // ...
        }
    }
     
    export default connectRequest({
        requestName: (props) => REQUEST_GET_TODO(props.params.id)
    })(MyTodoComponent);

    This will map the request tracking props to your Component

    Request tracking props

    • pending boolean (a request has been dispatched, pending resolve)
    • lastPage boolean (if link headers are being used, this will notify the component if the last page has been fetched)
    • requestDispatched boolean (determins if a namespaced request has ever been dispatched)

    Data accessors

    Now that all of your request data is managed in redux state, we need a way to access it in out views. The following are data accessors that come out of the box with redux-request-tracker, feel free to add your own or submit an isses if you would like to see any additions to this.

    getRequest

    Get request is your go to data accessor for determining the state of your request.

    const { getRequest } from 'redux-request-tracker';
     
    const mapStateToProps = (state) => {
        return {
            todosRequest: getRequest('REQUEST_GET_TODOS', state)
        };
    };

    getAppHistory

    This data accessor is more useful if you're analyzing your async events.

    const { getAppHistory } from 'redux-request-tracker';
     
    const mapStateToProps = (state) => {
        return {
            appHistory: getAppHistory(state)
        };
    };

    Pagination accessors

    These data accessors are responsible for parsing request pagination if supported. Check out the Pagination section for more information.

    const { getFirstPage, getLastPage, getNextPage, getPrevPage } from 'redux-request-tracker';
     
    const mapStateToProps = (state) => {
        return {
            nextPage: getNextPage('REQUEST_GET_TODOS', state),
            prevPage: getPrevPage('REQUEST_GET_TODOS', state),
            firstPage: getFirstPage('REQUEST_GET_TODOS', state),
            lastPage: getLastPage('REQUEST_GET_TODOS', state)
        };
    };

    Pagination

    redux-request-tracker automatically parses the Link Header if your server supports it. GitHub has a good example of how this works.

    Roadmap

    1. I want to add some components that are useful for working with the request history object returned from getAppHistory.

    Suggestions?

    Feel free to reach out with feedback and suggestions for improvement.

    Install

    npm i redux-request-tracker

    DownloadsWeekly Downloads

    18

    Version

    1.0.5

    License

    MIT

    Last publish

    Collaborators

    • oconn