Simple and powerful redux middleware that supports async side-effects (and much more)
Redux middleware that will allow you to:
- Create actions with side-effects that will dispatch different actions on different side-effect results
- Create actions with both async and sync side-effects
- Add hooks (callbacks) to you async actions so that you can treat dispatched actions like events and just react to them (e.g. show notification without checking if state changed)
There are some good middlewares that help manage side-effects in redux actions but they generally allow you to create actions creators that are vastly different then standard ones. I decided to make middleware with which you will be able to make action creators that will look consistent with the rest of the application and have all the other middlewares' power combined!
Yes and you should definitely check them out:
There are at least 3 options:
- Add an issue, write test(s) for bug you found, write fix that will make your test(s) pass, submit pull request
- Add an issue, write test(s) for bug you found, submit pull request with you test(s)
- Add an issue
All contributions are appreciated!
npm i redux-better-promise --save
import { createStore, applyMiddleware } from 'redux';
import createReduxPromise from 'redux-better-promise';
const store = createStore(
rootReducer,
applyMiddleware(createReduxPromise())
);
function getAsyncAction(param) {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
};
}
Dispatching action above will lead to dispatching following actions:
// before calling `promise` function
{
type: 'ACTION_STARTED',
}
// after promise is resolved
{
type: 'ACTION_SUCCEEDED',
result: { some: 'data' },
}
If promise returned by promise
function is rejected with { some: 'error' }
second action will look like this
// after promise is rejected
{
type: 'ACTION_ERROR',
error: { some: 'error' },
}
function getSyncAction(param) {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
function: ({ getState, dispatch }) => ({ some: 'data' }),
};
}
Dispatching action above will result in dispatching following actions:
// before `function` function is called
{
type: 'ACTION_STARTED',
}
// after `function` returns result
{
type: 'ACTION_SUCCEEDED',
result: { some: 'data' },
}
If function
throws a { some: 'error' }
second action will look like this
// after throwing an error
{
type: 'ACTION_ERROR',
error: { some: 'error' },
}
Hooks are just functions triggered when particular action is going to be dispatched. They cannot modify dispatching process in any way but can be used to trigger custom actions on some actions (like showing notification when async actions fails)
function getAsyncAction() {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
hooks: [() => notification('I started'), ({ result }) => notification('I finished successfully', result), () => notification('I failed :(')],
};
}
When dispatching action above notification('I started')
will be triggered immediately and notification('I finished successfully')
will be triggered after promise resolves.
notification('I failed')
will not be triggered unless promise fails
Start, success and error actions can contain additional data e.g. payload
function getAsyncAction() {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
payload: 'some payload',
whatever: 'some other data',
};
}
Here resulting actions would look like this:
// before calling `promise` function
{
type: 'ACTION_STARTED',
payload: 'some payload',
whatever: 'some other data',
}
// after promise is resolved
{
type: 'ACTION_SUCCEEDED',
result: { some: 'data' },
payload: 'some payload',
whatever: 'some other data',
}
types
and hooks
fields can be objects
function getAsyncAction() {
return {
types: { start: 'ACTION_STARTED', success: 'ACTION_SUCCEEDED', error: 'ACTION_ERROR' },
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
hooks: { start: () => notification('I started'), success: ({ result }) => notification('I finished successfully', result), error: () => notification('I failed :(') },
};
}
By default promise
and function
functions will get { getState, dispatch }
object as the first parameter. You can add any additional fields to that object:
const store = createStore(
rootReducer,
applyMiddleware(createReduxPromise({ myCustomParam: 'data', anotherData: 'value' }))
);
Now actions can look like this:
function getAsyncAction() {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
promise: ({ getState, dispatch, myCustomParam: 'data', anotherData: 'value' }) => Promise.resolve({ some: 'data' }),
};
}
or
function getSyncAction() {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
function: ({ getState, dispatch, myCustomParam: 'data', anotherData: 'value' }) => ({ some: 'data' }),
};
}
You don't have to provide all action types or hooks
function getAsyncAction() {
return {
types: [null, null, 'ACTION_ERROR'],
promise: ({ getState, dispatch }) => Promise.reject({ some: 'error' }),
hooks: [() => notification('I started'), null, null],
};
}
Now the only action dispatched will be 'ACTION_ERROR' (of course only if promise returned by promise
function will be rejected) and only before calling promise
function notification('I started')
hook is going to be called
Instead of providing action with types
field you can pass only one action type in type
field:
function getAsyncAction() {
return {
type: 'ACTION_SUCCESS',
promise: ({ getState, dispatch }) => Promise.reject({ some: 'error' }),
};
}
Now 'ACTION_SUCCESS' action would be triggered only if promise
action resolves. So in this case no action is going to be dispatched.
Instead of adding hooks to each action you can add global ones in hooks
field of a second argument of middlewareCreator:
import createReduxPromise, { actionTypes as reduxPromiseActionTypes } from 'redux-better-promise';
const store = createStore(
rootReducer,
applyMiddleware(createReduxPromise(null, { hooks: [
({ type, result, error, payload }) => console.log('I will be triggered on each and every action'),
{
actionType: reduxPromiseActionTypes.success,
hook: ({ result }) => console.log('I will be triggered on every success action'),
},
{
actionType: /^\w*ERROR$/,
hook: ({ error }) => console.log('I will be triggered only on actions ending in "ERROR"'),
},
{
actionType: (type, action) => !~(['ONE_ACTION', 'DIFFERENT_ACTION']).indexOf(type),
hook: () => console.log('I will be triggered on all actions except ONE_ACTION and DIFFERENT_ACTION'),
},
{
actionType: ['ONE_ACTION', 'DIFFERENT_ACTION'],
hook: () => console.log('I will be triggered on ONE_ACTION and DIFFERENT_ACTION only'),
}
] }))
);
You can also replace actionType with actionTypeExclude which will just inverts the behaviour of that field or even use both fields (the exclude
field will overwrite the include
filed):
import createReduxPromise, { actionTypes as reduxPromiseActionTypes } from 'redux-better-promise';
const store = createStore(
rootReducer,
applyMiddleware(createReduxPromise(null, { hooks: [
({ type, result, error, payload }) => console.log('I will be triggered on each and every action'), // note that global hooks gets `type` in the first parameter wheras per-action hooks do not
{
actionTypeExclude: reduxPromiseActionTypes.success,
hook: ({ result }) => console.log('I will be triggered on every start and error action'),
},
{
actionTypeExclude: /^\w*ERROR$/,
hook: ({ error }) => console.log('I will be triggered only on actions not ending in "ERROR"'),
},
{
actionTypeExclude: (type, action) => !~(['ONE_ACTION', 'DIFFERENT_ACTION']).indexOf(type),
hook: () => console.log('I will be triggered on ONE_ACTION and DIFFERENT_ACTION only'),
},
{
actionType: ['ONE_ACTION', 'DIFFERENT_ACTION'],
actionTypeExclude: (type, action) => action.payload,
hook: () => console.log('I will be triggered on ONE_ACTION and DIFFERENT_ACTION only if the do not have `payload` field'),
}
] }))
);
There are different generic types in actionTypes named export object:
import { actionTypes } from 'redux-better-promise';
actionTypes.start;
actionTypes.success;
actionTypes.error;
actionTypes.finish; // success and error
Most of the time it's better not to have fields named function
in you code ;)
You can change all default field names by providing any number of configFields as a second argument to middlewareCreator
const store = createStore(
rootReducer,
applyMiddleware(createReduxPromise(null, { functionFieldName: 'myNewName' }))
);
Now actions can look like this:
function getSyncAction() {
return {
types: ['ACTION_STARTED', 'ACTION_SUCCEEDED', 'ACTION_ERROR'],
myNewName: ({ getState, dispatch }) => ({ some: 'data' }),
};
}
Your config object will be deeply merged with default options:
{
promiseFieldName: 'promise',
functionFieldName: 'function',
typesFieldName: 'types',
hooksFieldName: 'hooks',
typesNames: { // used when types field is object
start: 'start',
success: 'success',
error: 'error',
},
hooksNames: { // used when hooks field is object
start: 'start',
success: 'success',
error: 'error',
},
}
Note that you cannot change type
field name!
Theoretically you could create actions that does not influence redux store. For example:
function getAsyncAction() {
return {
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
};
}
When dispatching action above no action will actually be dispatched on redux store. As using redux in this way doesn't make sense, actions that has neither types
nor type
field will throw an error when dispatched.
Note that you still can make a workaround:
function getAsyncAction() {
return {
types: [ null, null, null ],
promise: ({ getState, dispatch }) => Promise.resolve({ some: 'data' }),
};
}
Although in this version such action will not throw an error it is generally bad idea to create it. Even with hooks, it's much better (and easier to reason about) when calling promise directly.
As with redux-thunk
or redux-saga
you can create action that will conditionally dispatch other actions:
function getWrapperAction(makeItSo) {
return {
function: ({ dispatch }) => makeItSo ? dispatch(someOtherAction) : null,
};
}
Most of the time it's not a good idea. It's probably better to just conditionally dispatch someOtherAction than to create wrapper action to do that for you.
- Errors in success hooks should not be caught together with errors in
function
- Refactor tests (merge some tests and split others to make them more readable)
- You will not be able to dynamically add hooks (like listeners) to actions (e.g. with middleware.addHook()) except in action creators. The power of redux is in the ability to quickly find the reason why something happened and adding dynamic listeners will make the code hard to understand.