Last active
June 25, 2018 19:56
-
-
Save berdyshev/bc13bb94e26e9a89fcfe2726bf5d4c81 to your computer and use it in GitHub Desktop.
Queue of API requests using redux-saga
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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