Skip to content

Instantly share code, notes, and snippets.

@marcusradell
Created November 29, 2017 05:55
Show Gist options
  • Save marcusradell/0bdc7e099cc28027d0ebcf066ad74fed to your computer and use it in GitHub Desktop.
Save marcusradell/0bdc7e099cc28027d0ebcf066ad74fed to your computer and use it in GitHub Desktop.
// @NOTE: This is an experimental thought on removing some imperative details from the Redux setup.
import { takeLatest, put, call } from 'redux-saga/effects'
// @TODO(MANI): Support an apiAction per baseActionType
export const makeAsyncMakeSagas = (baseActionType, asyncActionTypes) => {
const makeSagas = apiAction => {
function* loadSaga(payload) {
const result = yield call(apiAction, payload)
const { success, data, errors } = result
yield success
? put({
type: asyncActionTypes[`${baseActionType}_COMPLETED`],
payload: data,
})
: put({
type: asyncActionTypes[`${baseActionType}_FAILED`],
payload: errors,
})
}
function* watchLoadSaga() {
yield takeLatest(baseActionType, loadSaga)
}
return {
loadSaga,
watchLoadSaga,
}
}
return makeSagas
}
export const makeAsyncInitialState = baseInitialState => ({
...baseInitialState,
pristine: true,
loading: false,
errors: null,
})
export const loadingUpdater = state => ({
...state,
loading: true,
errors: null,
})
export const failedUpdater = (state, errors) => ({
...state,
loading: false,
errors,
})
export const completedUpdater = (state, successPayloadObject) => ({
...state,
pristine: false,
loading: false,
errors: null,
...successPayloadObject,
})
export const makeAsyncActionType = actionType => ({
[actionType]: actionType,
[`${actionType}_COMPLETED`]: `${actionType}_COMPLETED`,
[`${actionType}_FAILED`]: `${actionType}_FAILED`,
})
export const makeAsyncUpdater = actionType => ({
[actionType]: loadingUpdater,
[`${actionType}_COMPLETED`]: completedUpdater,
[`${actionType}_FAILED`]: failedUpdater,
})
// @TODO(MANI): Split into two factories?
export const makeAsyncActionTypesAndUpdaters = actionTypes =>
actionTypes.reduce(
(acc, actionType) => ({
actionTypes: {
...acc.asyncActionTypes,
...makeAsyncActionType(actionType),
},
updaters: {
...acc.asyncUpdaters,
...makeAsyncUpdater(actionType),
},
}),
{ actionTypes: {}, updaters: {} }
)
export const makeAsyncReducers = (initialState, updaters) => {
const reducers = (state = initialState, action) => {
const { type, payload } = action
return updaters[type] ? updaters[type](state, payload) : state
}
return reducers
}
export const makeAsyncModel = (baseInitialState, baseActionTypes) => {
const baseActionTypesArray = Array.isArray(baseActionTypes)
? baseActionTypes
: [baseActionTypes]
const initialState = makeAsyncInitialState(baseInitialState)
const { actionTypes, updaters } = makeAsyncActionTypesAndUpdaters(
baseActionTypesArray
)
const actionCreators = baseActionTypesArray.reduce(
(acc, actionType) => ({
...acc,
[actionType
.toLowerCase()
.replace(/_([a-z])/g, g => g[1].toUpperCase())]: payload => ({
type: actionType,
payload,
}),
}),
{}
)
const reducers = makeAsyncReducers(initialState, updaters)
// @TODO(MANI): Make it for all the baseActionTypes in the array
const makeSagas = makeAsyncMakeSagas(baseActionTypesArray[0], actionTypes)
return {
actionTypes,
actionCreators,
reducers,
makeSagas,
}
}
import { call } from 'redux-saga/effects'
import { makeAsyncModel } from '../container-factory'
test('makeAsyncModel', () => {
const asyncModel = makeAsyncModel({ value: null }, 'SET_VALUE')
expect(asyncModel).toMatchSnapshot()
})
test('asyncModel.reducers', () => {
const asyncModel = makeAsyncModel({ value: null }, 'SET_VALUE')
const expectedInitialState = {
value: null,
errors: null,
loading: false,
pristine: true,
}
const actualInitialState = asyncModel.reducers(undefined, { type: '@@INIT' })
expect(actualInitialState).toEqual(expectedInitialState)
})
test('asyncModel.reducers', () => {
const asyncModel = makeAsyncModel({ value: null }, 'SET_VALUE')
const expectedState = {
errors: null,
loading: true,
pristine: true,
value: null,
}
const initialState = asyncModel.reducers(undefined, { type: '@@INIT' })
const actualState = asyncModel.reducers(
initialState,
asyncModel.actionCreators.setValue()
)
expect(actualState).toEqual(expectedState)
})
test('asyncModel.makeSagas().loadSaga', () => {
const apiActionMock = payload => Promise.resolve(payload)
const payloadMock = { id: 'test-id' }
const asyncModel = makeAsyncModel({ value: null }, 'SET_VALUE')
const { loadSaga } = asyncModel.makeSagas(apiActionMock)
const iterator = loadSaga(payloadMock)
const result = iterator.next().value
expect(result).toEqual(call(apiActionMock, payloadMock))
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment