Skip to content

Instantly share code, notes, and snippets.

@berdyshev
Last active June 25, 2018 19:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save berdyshev/bc13bb94e26e9a89fcfe2726bf5d4c81 to your computer and use it in GitHub Desktop.
Save berdyshev/bc13bb94e26e9a89fcfe2726bf5d4c81 to your computer and use it in GitHub Desktop.
Queue of API requests using redux-saga
import { createRequestTypes } from 'utils/actionHelpers';
import { typeValidator } from 'utils/typeValidator';
import { createAction } from 'redux-actions';
import { createApiCall } from 'utils/api';
//region ACTION TYPES
const tokenTypes = {
SIGN_IN: createRequestTypes('SIGN_IN'),
SIGN_OUT: 'SIGN_OUT',
UPDATE_TOKEN: 'UPDATE_TOKEN',
};
export const AUTH = new Proxy(tokenTypes, typeValidator);
//endregion
//region ACTIONS
export const signIn = createApiCall(AUTH.SIGN_IN, (username, password) => ({
url: '/users/login',
method: 'POST',
data: { username, password },
anon: true,
}));
import {
actionChannel,
call,
select,
takeEvery,
put,
flush,
take,
} from 'redux-saga/effects';
import { LOCATION_CHANGE } from 'react-router-redux';
import {
AUTH,
getAccessToken,
getRefreshToken,
updateAccessToken,
signOut,
} from 'modules/auth';
import {
addNotification,
notificationFromErrorHelper,
} from 'modules/notifications';
import { API, apiClient } from 'utils/api';
function* request({ anon, ...options }) {
options.data = options.data || {};
options.headers = options.headers || {};
// add auth header if required.
if (!anon && !options.headers.Authorization) {
const token = yield select(getAccessToken);
options.headers.Authorization = `Bearer ${token}`;
}
return yield apiClient
.request(options)
.then(response => [(response.data && response.data.data) || {}])
.catch(error => [
false,
error.response
? error.response.data
: { message: error.message, status: error.code || 500 },
]);
}
/**
* Generates and handles api call in generic way.
*/
function* apiCall(options, { actions, ...meta } = {}) {
if (actions && actions.REQUEST) {
yield put({ type: actions.REQUEST, payload: options, meta });
}
while (true) {
const [payload, error = {}] = yield request(options);
if (payload) {
if (actions && actions.SUCCESS) {
yield put({ type: actions.SUCCESS, payload, meta });
}
break;
} else if (options.anon || error.status !== 401) {
if (actions && actions.FAILURE) {
yield put({ type: actions.FAILURE, payload: error, meta });
yield put(addNotification(notificationFromErrorHelper(error)));
}
break;
}
const refreshToken = yield select(getRefreshToken);
const [refreshTokenPayload] = yield request({
url: '/users/token/refresh',
method: 'POST',
headers: {
Authorization: `Bearer ${refreshToken}`,
},
});
if (!refreshTokenPayload) {
yield put(signOut());
break;
}
yield put.resolve(updateAccessToken(refreshTokenPayload.access_token));
}
}
/**
* Main Saga to track all api calls.
*/
export function* watchApiCall() {
// handle all API.CALL like a queue.
const apiCallsChannel = yield actionChannel(API.CALL);
yield takeEvery([LOCATION_CHANGE, AUTH.SIGN_OUT], function*(action) {
if (
action &&
action.type === LOCATION_CHANGE &&
action.payload.state &&
!!action.payload.state.doNotInterruptRequests
) {
return;
}
yield flush(apiCallsChannel);
});
while (true) {
const { payload, meta } = yield take(apiCallsChannel);
yield call(apiCall, payload, meta);
}
}
import axios from 'axios';
import { createAction } from 'redux-actions';
import { typeValidator } from './typeValidator';
const apiTypes = {
CALL: 'API_CALL',
READ: 'GET',
UPDATE: 'PUT',
DELETE: 'DELETE',
CREATE: 'POST',
};
export const API = new Proxy(apiTypes, typeValidator);
/**
* Generic helper to create payload for the api request action.
*
* @param {Array} apiActions Set of request actions to dispatch after api call.
* @param {object} options Parameter for axios.
* @param {object} meta (optional)
*/
export const createApiCall = (apiActions, optionsCb, meta = {}) => {
return createAction(
API.CALL,
(...params) => {
switch (typeof optionsCb) {
case 'function':
return optionsCb(...params);
case 'string':
return { url: optionsCb };
default:
return optionsCb;
}
},
(...params) => ({
...(typeof meta === 'function' ? meta(...params) : meta),
actions: apiActions,
})
);
};
// Configure axios client.
export const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
apiClient.defaults.headers.common['Content-Type'] = 'application/json';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment