redux-promise-memo
redux-promise-memo
lets you "memoize" asynchronous,
promise-based, Redux action creators. If the
promise has completed successfully, it will not be dispatched again unless the
action creator arguments change. The memoized action creator can be called
multiple times (e.g. in React's componentDidUpdate
) and it will do nothing
unless the arguments change.
"memoize" is in quotes because it remembers metadata about the action creator
but not the actual data. Apps that use Redux are already "caching" the data in
Redux state.
- it also does not dispatch duplicate actions if the promise is in a pending state
(e.g. API data is loading)
- it works stand-alone, with
redux-promise-middleware
,
GlueStick's
promiseMiddleware
,
or redux-pack
. (Other promise
middleware may be used if appropriate initMatcher
, failureMatcher
, and
successMatcher
functions can be written. See the documentation for createMemoReducer
below.) - it works with client side and server side rendering (universal / isomorphic apps)
because metadata is stored in Redux state
- it supports either a single cache per action type or infinite caches per action type
(see
multipleCaches
option passed to memoize
) - it supports cache invalidation by providing an app-specific invalidation function
(see
invalidate
config passed to createMemoReducer
) - it is 1.1 kB minified + gzipped
Inspiration
redux-promise-memo
was inspired by Dan Abramov's comment about redux-thunk
:
There are differing opinions on whether accessing state in action creators is
a good idea. The few use cases where I think it’s acceptable is for checking
cached data before you make a request, or for checking whether you are
authenticated...
from https://stackoverflow.com/questions/35667249/accessing-redux-state-in-an-action-creator/35674575#35674575
How it works
- middleware is
used to dispatch an action for each of the 3 promise states
- a reducer stores
the status of each promise per a memoization key and argument list in the Redux state.
- the action creator decorator
reads the Redux state (using
redux-thunk
) and dispatches the action if the action
creator arguments have changed or does nothing if not.
Install
redux-promise-memo
uses redux-thunk
so if you are not already using
redux-thunk
, you must install it as well.
npm install redux-promise-memo redux-thunk
or
yarn add redux-promise-memo redux-thunk
Usage (stand alone)
path/to/your/root/reducer
:
import { combineReducers } from "redux";
import { createMemoReducer } from "redux-promise-memo";
const _memo = createMemoReducer();
const your = (state, action) => ({});
const other = (state, action) => ({});
const reducers = (state, action) => ({});
const rootReducer = combineReducers({_memo, your, other, reducers});
export default rootReducer;
configureStore.js
:
import { applyMiddleware, createStore } from "redux";
import { createMemoReducer, promiseMiddleware } from "redux-promise-memo";
import thunk from "redux-thunk";
import reducer from "./path/to/your/root/reducer";
const middleware = [thunk, promiseMiddleware];
const store = createStore(reducer, applyMiddleware(...middleware));
actions.js
:
import { memoize } from "redux-promise-memo";
const fetchSomething = (id) => ({
type: "FETCH_SOMETHING",
promise: fetch("/my/url")
});
export const memoizedFetchSomething = memoize(fetchSomething, "FETCH_SOMETHING");
Then use memoizedFetchSomething
the same way you would have used fetchSomething
.
See examples/basic-example/src/index.js
for a full working example.
Examples
Assumptions / limitations
- it does not work with
redux-thunk
action creators - if using server rendering, the promise returned when dispatching the action is
used for timing purposes only (i.e. the promise value is not used). The return
value of the action dispatch is stored in a variable in the
memoize
function
and used via closure. When transitioning from server rendering to client
rendering, the Redux state is preserved, allowing for memoization from server
to client. But the return value variable is not preserved so the promise
resolved value cannot be used. An empty resolved promise is returned instead.
API
-
createMemoReducer(config)
Return a reducer that must be used with the `memoize` decorator.
IMPORTANT: the reducer must be added to the root reducer using the key name `_memo`.
`config` (Object):
{
invalidate: action => boolean | Array<string>,
initMatcher: action => boolean,
failureMatcher = action => boolean,
successMatcher = action => boolean,
}
invalidate should return true if all the keys in the _memo state should be cleared.
If it returns an array of keys, only those keys will be cleared.
initMatcher should return true if the init action was dispatched
failureMatcher should return true if the failure action was dispatched
successMatcher should return true if the success action was dispatched
-
defaultConfig
- this is the default config that is used if no config object is
passed to createMemoReducer
-
reduxPromiseMiddlewareConfig
- this config is to be used with redux-promise-middleware
-
memoize(actionCreator, key, [options])
Return a "memoized" version of a promise action creator.
The memoized version will not dispatch the action if:
- the promise has not resolved (e.g. api request is still loading)
- the action has already been dispatched with the same arguments
- the original action creator returns null (or a falsey value)
Assumptions:
- the action creator returns a simple object (i.e. it is not a thunk action creator)
`actionCreator` (function that returns an object literal) - the action creator to
be "memoized"
`key` (string) - the primary memoization key. It is recommended to use the action type
as the key. The secondary memoization key is the argument list passed to `JSON.stringify`.
`options` (Object):
`multipleCaches` option:
Normally `memoize` only checks if the current call matches the immediately preceding
call and re-fetches if it doesn't.
For example, if an action is called with a set of arguments, then called with a second
set of arguments, then called with the first set of arguments again, the last call will
be re-fetched. If you store the responses for each API call separately, then
set the `multipleCaches` option to true and `memoize` will always use the cached
version if the arguments match.
Usage:
const fetchSomething = memoize(_fetchSomething, "FETCH_SOMETHING");
or
const fetchSomething = memoize(_fetchSomething, "FETCH_SOMETHING" {multipleCaches: true});
-
promiseMiddleware
For a given action:
const fetchSomething = () => ({
type: "FETCH_SOMETHING",
promisse: fetch("/something")
});
promiseMiddleware will dispatch 2 actions. The first is dispatched immediately:
{
type: "FETCH_SOMETHING_INIT"
}
the second action is dispatched either after the promise is fulfilled:
{
type: "FETCH_SOMETHING",
payload: "the payload is set to the resolved value of the promise"
}
or when the promise is rejected:
{
type: "FETCH_SOMETHING_FAILURE",
error: "error goes here"
}
promiseMiddleware was copied from GlueStick
Changelog
0.2.3
- add an error for thunk action creators
0.2.2
- configure and run Prettier
- upgrade Flow and remove $FlowFixMe comment
0.2.1
0.2.0
- add support for invalidating only specific keys using the
invalidate
function passed to
createMemoReducer
. - Breaking: change
invalidate
function signature to only accept the action instead of the
state and the action.
0.1.0
- store action creator arguments in Redux instead of testing if arguments are
equal using
lodash.isEqual
. This allows removing the external lodash dependency
and removes doing the potentially expensive deep comparison. - manually specify memoization key so that
promise
can be a promise instead of
a function returning a promise - simplify data stored from an object of 3 booleans to a string having 3 values
- add config option to provide support for other middleware
before 0.1.0
To do
- I tried to write a Redux store enhancer to make configuration less tedious but
I did not get it to work with other middleware. If you know the problem,
please let me know.