redux-action-handlers
v1.0.1
Published
Redux actionHandlers object
Downloads
1
Readme
ReduxActionHandlers
ActionHandlers library to improve the Redux development experience
How to use this:
- Put all Your reducer cases into functions, and put them in the action file
src/actions/downloadSomething.js
export function downloadSomethingRequest_actionHandler(state, action) {
return {
...state,
registration: action.payload,
isFetching: true,
data: {}
}
}
- Add them to actionHandlers (for reducer "something", map "DOWNLOAD_SOMETHING_REQUEST" action type to "downloadSomethingRequest_actionHandler" function)
actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)
- Add handling to reducer
const reducer = (state = initialState, action = {}) => {
return actionHandlers.runReducer('something', state, action);
}
Benefits of using this library:
- One file describing all the functionality of an action. Instead of splitting the action creator logic and reducer logic to two separate files, one concern is in one file.
- Huge reducers are no longer a problem. Instead of having all the reducer handling code in one huge switch...case block, each action is in it's own function which makes it cleaner.
- When using combineReducers, You create shards. Those shards are enforced in actionHandlers. This means that You will never have a polluted root reducer namespace
Compatibility:
- This library has no dependencies
- It does not force the ActionHandlers on Your whole project. You can still use both classic reducer switch...case and ActionHandlers.
- It does not pollute the redux state with functions.
- It's a clean library - it supports all use cases.
- Supports nested combineReducers
- Supports many-to-many relationships between actions and reducer cases.
- It's tiny. All it takes is 31 lines of code.
- Testing is unchanged, but can be optimized. By testing the reducer functions directly, tests can become much clearer, but this library doesn't force it. You can test the whole reducer.
ReduxActionHandlers reason to exist
To create a single action and a reducer, you have to create multiple files and put a part of the implementation in each of them:
src/actions/downloadSomething_types.js
export const DOWNLOAD_SOMETHING_REQUEST = '@request/DOWNLOAD_SOMETHING_REQUEST'
export const DOWNLOAD_SOMETHING_SUCCESS = '@request/DOWNLOAD_SOMETHING_SUCCESS'
export const DOWNLOAD_SOMETHING_FAILURE = '@request/DOWNLOAD_SOMETHING_FAILURE'
src/actions/downloadSomething.js
import {
DOWNLOAD_SOMETHING_REQUEST,
DOWNLOAD_SOMETHING_SUCCESS,
DOWNLOAD_SOMETHING_FAILURE
} from 'src/actions/downloadSomething_types.js'
export default function downloadSomethingAction() {
return (dispatch) => {
dispatch({
type: DOWNLOAD_SOMETHING_REQUEST
})
return api.downloadSomething()
.then(
json => dispatch({
type: DOWNLOAD_SOMETHING_SUCCESS,
payload: json
}),
exception => {
dispatch({
type: DOWNLOAD_SOMETHING_FAILURE
})
dispatch(notifyError(exception.message))
}
)
}
}
src/reducers/downloadReducer.js
import {
DOWNLOAD_SOMETHING_REQUEST,
DOWNLOAD_SOMETHING_SUCCESS,
DOWNLOAD_SOMETHING_FAILURE
} from 'actions/downloadSomething_types.js'
export const initialState = {
isFetching: false,
data: {},
registration: null,
visible: false
};
const reducer = (state = initialState, action = {}) => {
if (!action.type) return state
switch (action.type) {
case DOWNLOAD_SOMETHING_REQUEST:
return {
...state,
registration: action.payload,
isFetching: true,
data: {}
}
case DOWNLOAD_SOMETHING_SUCCESS:
return {
...state,
data: action.payload,
isFetching: false
}
case DOWNLOAD_SOMETHING_FAILURE:
return {
...state,
data: {},
isFetching: false
}
default:
return state
}
}
export default reducer
This single concern, single chunk of functionality is spread over three separate files.
It makes it difficult to reason about and debug. The reducer construction is also an issue because it very easily can grow into a monster like this (and this has a potential to be much bigger):
import {
OPEN_INFO_PANEL,
CLOSE_INFO_PANEL,
READ_INFO_REQUEST,
READ_INFO_SUCCESS,
READ_INFO_FAILURE,
UPDATE_TIMES_REQUEST,
UPDATE_TIMES_SUCCESS,
UPDATE_TIMES_FAILURE,
UPDATE_POSITION_STAND_REQUEST,
UPDATE_POSITION_STAND_SUCCESS,
UPDATE_POSITION_STAND_FAILURE
} from 'features/info/actions'
export const defaultState = {
data: {},
flightId: null,
isFetching: false,
isUpdating: false,
visible: false
}
export default (state = defaultState, action = {}) => {
if (!action.type) return state
switch(action.type) {
case OPEN_INFO_PANEL:
return {
...state,
flightId: action.payload,
isFetching: false,
visible: true
}
case CLOSE_INFO_PANEL:
return {
...state,
flightId: null,
isFetching: false,
visible: false
}
case READ_INFO_REQUEST:
return {
...state,
data: {},
flightId: action.payload,
isFetching: true
}
case READ_INFO_SUCCESS:
return {
...state,
data: action.payload,
isFetching: false
}
case READ_INFO_FAILURE:
return {
...state,
data: {},
isFetching: false
}
case UPDATE_TIMES_SUCCESS:
return {
...state,
isUpdating: false,
data: { ...state.data,
times: {...state.data.times,
actual: {...action.payload.actualTimes}
}
}
}
case UPDATE_POSITION_STAND_SUCCESS:
return {
...state,
isUpdating: false,
data: {
...state.data,
gatesInfo: {
...action.payload
}
}
}
case UPDATE_TIMES_REQUEST:
return {
...state,
isUpdating: true
}
case UPDATE_POSITION_STAND_REQUEST:
return {
...state,
isUpdating: true
}
case UPDATE_TIMES_FAILURE:
return {
...state,
isUpdating: false
}
case UPDATE_POSITION_STAND_FAILURE:
return {
...state,
isUpdating: false
}
default:
return state
}
}
Solution:
ReduxActionHandlers!
Idea:
Put all the reducer cases into functions like so:
Before:
case DOWNLOAD_SOMETHING_REQUEST:
return {
...state,
registration: action.payload,
isFetching: true,
data: {}
}
After:
function downloadSomethingRequest_actionHandler(state, action) {
return {
...state,
registration: action.payload,
isFetching: true,
data: {}
}
}
Then, instead of attaching actions externally ( from reducer ) we would attach actions to a reducer like so:
actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)
Resulting files would be as follows:
src/actions/downloadSomething.js
import actionHandlers from 'redux-action-handlers'
const DOWNLOAD_SOMETHING_REQUEST = '@request/DOWNLOAD_SOMETHING_REQUEST'
const DOWNLOAD_SOMETHING_SUCCESS = '@request/DOWNLOAD_SOMETHING_SUCCESS'
const DOWNLOAD_SOMETHING_FAILURE = '@request/DOWNLOAD_SOMETHING_FAILURE'
export function downloadSomethingRequest_actionHandler(state, action) {
return {
...state,
registration: action.payload,
isFetching: true,
data: {}
}
}
export function downloadSomethingSuccess_actionHandler(state, action) {
return {
...state,
data: action.payload,
isFetching: false
}
}
export function downloadSomethingFailure_actionHandler(state, action) {
return {
...state,
data: {},
isFetching: false
}
}
export default function downloadSomethingAction() {
return (dispatch) => {
dispatch({
type: DOWNLOAD_SOMETHING_REQUEST
})
return api.downloadSomething()
.then(
json => dispatch({
type: DOWNLOAD_SOMETHING_SUCCESS,
payload: json
}),
exception => {
dispatch({
type: DOWNLOAD_SOMETHING_FAILURE
})
dispatch(notifyError(exception.message))
}
)
}
}
actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)
actionHandlers.add('something', DOWNLOAD_SOMETHING_SUCCESS, downloadSomethingSuccess_actionHandler)
actionHandlers.add('something', DOWNLOAD_SOMETHING_FAILURE, downloadSomethingFailure_actionHandler)
src/reducers/downloadSomethingReducer.js
import actionHandlers from 'redux-action-handlers'
export const initialState = {
isFetching: false,
data: {},
registration: null,
visible: false
};
const reducer = (state = initialState, action = {}) => {
return actionHandlers.runReducer('something', state, action);
}
export default reducer