Skip to content

Instantly share code, notes, and snippets.

@betolink
Last active September 7, 2016 19:22
Show Gist options
  • Save betolink/79b06c68f2fc9885051a825c1369ff18 to your computer and use it in GitHub Desktop.
Save betolink/79b06c68f2fc9885051a825c1369ff18 to your computer and use it in GitHub Desktop.

nsidc-fetch-dispatch


Motivation

You want to call an API from your redux application and you decide to use redux-saga or redux-thunk, then to use it in your app you decide to encapsulate the logic in a module that talks the API language. This is good enough but then what if you want to consume a different API (say you need data from data.gov and nsidc.org) then you add more methods to your API wrapper or you even create a new API wrapper.

That's all good but maybe we can use a more generic approach to consume APIs. First let's take a look at what a normal redux-thunk use case would look like:

API.js

import ACTIONS from 'MyCustomActions';
import fetch from 'some-async-library';

function parseCollections (collections) {
  return function (dispatch, getState) {
    dispatch({
      type: ACTIONS.COLLECTION_LOADED,
      data: collections
    }); 
}

 export function loadCollections(config) {
  return fetch(config), {
    url: config.url,
    headers: config.headers
  }).then(response => response.json())
    .then(response => parseCollections(response));
}

ACTIONS.js:

import loadCollections from 'API';

function fetchCollections( dispatch, getState ) {
     let state = getState();
     if (someConditional()) {
         dispatch(loadCollections(params));
     }
}

REDUCER.js

function handleCollections(state, action) {
switch (action.type) {
   case ACTIONS.COLLECTIONS_LOADED:
     return {
       data: action.data
     };
   default:
    return state;
  };
}

This works, in React we normally bind fetchCollections() to our UI so it can be triggered by a synthetic event, i.e. componentWillMount, or onClick etc. However there are some caveats with this approach:

  • The API wrapper is aware of the redux constants and cannot be reused as is for a different API or with a different reducer.
  • Testing our async calls becomes hard because of the dependencies on the API wrapper.
  • There is a one to one relationship from and endpoint to a function, i.e. /user/{id} will have a getUser(id), postUser(userInfo), update(userId, userInfo) each invokes an async library directly and if the API gets modified i.e. they decided to replace a POST for a PUT then we'll have to change all of our calls. No big deal but probably not the best abstraction level is being used.

Using nsidc-fetch-dispatch

API.js

export function getApiParams (action, params) {
  baseUrl: 'https://myAPI.org',
  auth: {},
  swtich (action) {
  case 'postUser': return {
      url: `${baseUrl}/user/${params.userId}`,
      method: 'POST',
      body: params.userInfo,
      headers: {},
      auth: {someAuthToken}
    },
  case 'loadCollections': return {
    url: `${baseUrl}/collections/`,
    method: 'GET',
    headers: {}
  }
  };
}

ACTIONS.js:

import fetchDispatch from 'nsidc-fetch-dispatch';
import getApiParams from 'API';
import ACTIONS from 'MyCustomActions';

function loadCollections () {
  return (dispatch, getState) => {
    const params = {
      reqParams: getApiParams('loadCollections'),
      requestAction: ACTIONS.REQUEST_COLLECTIONS,
      receiveAction: ACTIONS.RECEIVE_COLLECTIONS,
      errorAction: ACTIONS.LOAD_COLLECTIONS_ERROR
    }
    return dispatch(fetchDispatch(params));
  };
}

REDUCER.js

function handleActions (state, action) {
  switch (action.type) {
  case ACTIONS.LOAD_COLLECTIONS_ERROR:
    return { 
      error: true,
      errorMessage: action.error
    };
  case ACTIONS.REQUEST_COLLECTIONS:
    return { isFetching: true };
  case ACTIONS.RECEIVE_COLLECTIONS:
    return {
      isFetching: false,
      data: action.data
    };
  default:
    return state;
  }
}

function collectionReducer (state = {}, action) {
  return Object.assign({}, state, handleActions(state, action));
}

export default collectionReducer;

What did we gain from using nsidc-fetch-dispatch?

  • API.js now is totally unaware of anything but the API itself.
  • We can stack multiple routers to different APIs
  • Since there are no ACTIONS' dependencies on the API we can test async calls in a more convenient way
  • We can reuse our API definition with different reducers!

making the world a better palce

TDD

Testing our async calls is now a simpler task:

import chai from 'chai';
let expect = chai.expect;
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import loadCollections from '../actions';
import ACTIONS from '../MyCustomActions';

const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);

describe('Async Actions', () => {
  afterEach(() => {
    nock.cleanAll();
  });

  it('creates REQUEST_COLLECTIONS when we invoke loadCollections() and ' + 
     'creates RECEIVE_COLLECTIONS when loadCollections() finishes', () => {
    nock('http://localhost:8080')
      .get('/collections')
      .reply(200, { data: [{'some data'}] });

    const expectedActions = [
      { type: ACTIONS.REQUEST_COLLECTIONS },
      { type: ACTIONS.RECEIVE_COLLECTIONS, collections: [{'some data'}]}
    ];
    const store = mockStore({ table: {collections: [], isFetching: false }});

    return store.dispatch(loadCollections())
      .then(() => {
        expect(store.getActions()).to.eql(expectedActions);
      });
  });
});

TODO

Contributing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment