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 agetUser(id)
,postUser(userInfo)
,update(userId, userInfo)
each invokes an async library directly and if the API gets modified i.e. they decided to replace aPOST
for aPUT
then we'll have to change all of our calls. No big deal but probably not the best abstraction level is being used.
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;
- 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!
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);
});
});
});