Redux module that makes working with APIs a breeze.
Caching API responses can greatly increase UX by saving network
bandwidth and not showing loaders for the same resources all over again while
user navigates the application. You can also create a fluid returning UX in
combination with persistance libraries, e.g., redux-persist
.
The redux-api-middleware
library is pretty
standardized and popular way to interact with APIs using redux, that's why it was
chosen as a base for this package.
- Install dependencies:
$ npm install --save redux-cached-api-middleware redux-api-middleware redux-thunk
or
$ yarn add redux-cached-api-middleware redux-api-middleware redux-thunk
* You can also consume this package via <script>
tag in browser from UMD
build. The UMD builds make redux-cached-api-middleware available as a
window.ReduxCachedApiMiddleware global variable.
- Setup
redux
:
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { apiMiddleware } from 'redux-api-middleware';
import api from 'redux-cached-api-middleware';
import reducers from './reducers';
const store = createStore(
combineReducers({
...reducers,
[api.constants.NAME]: api.reducer,
}),
applyMiddleware(thunk, apiMiddleware)
);
A simple ExampleApp
component that invokes API endpoint on mount with TTL_SUCCESS
cache strategy of 10 minutes. This means that if items were fetched in the past
10 minutes successfully, the cached value will be returned, otherwise new fetch
request will happen.
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import api from 'redux-cached-api-middleware';
import Items from './Items';
import Error from './Error';
class ExampleApp extends React.Component {
componentDidMount() {
this.props.fetchData();
}
render() {
const { result } = this.props;
if (!result) return null;
if (result.fetching) return <div>Loading...</div>;
if (result.error) return <Error data={result.errorPayload} />;
if (result.successPayload) return <Items data={result.successPayload} />;
return <div>No items</div>;
}
}
ExampleApp.propTypes = {
fetchData: PropTypes.func.isRequired,
result: PropTypes.shape({
fetching: PropTypes.bool,
fetched: PropTypes.bool,
error: PropTypes.bool,
timestamp: PropTypes.number,
successPayload: PropTypes.any,
errorPayload: PropTypes.any,
}),
};
const CACHE_KEY = 'GET/items';
const enhance = connect(
(state) => ({
result: api.selectors.getResult(state, CACHE_KEY),
}),
(dispatch) => ({
fetchData() {
return dispatch(
api.actions.invoke({
method: 'GET',
headers: { Accept: 'application/json' },
endpoint: 'https://my-api.com/items/',
cache: {
key: CACHE_KEY,
strategy: api.cache
.get(api.constants.CACHE_TYPES.TTL_SUCCESS)
.buildStrategy({ ttl: 10 * 60 * 1000 }), // 10 minutes
},
})
);
},
})
);
export default enhance(ExampleApp);
The default redux-api-middleware RSAA options object
that later will be merged when calling every invoke
action - e.g.:
api.config.DEFAULT_INVOKE_OPTIONS = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
};
* Options get merged using Object.assign({}, DEFAULT_INVOKE_OPTIONS, invokeOptions)
in invoke
action.
The default caching strategy that will be used when
calling every invoke
action - e.g.:
api.config.DEFAULT_CACHE_STRATEGY = api.cache
.get(api.constants.CACHE_TYPES.TTL_SUCCESS)
.buildStrategy({ ttl: 600000 });
Call API endpoints anywhere and retrieve data with redux selectors.
dispatch(api.actions.invoke((options: InvokeOptions)));
The invoke
action response will be undefined
if there was a valid cached
value in redux state, otherwise invoke
will return redux-api-middleware
response.
InvokeOptions
is an extended version of redux-api-middleware options.
You can use invoke
like an RSAA
action wrapper without any caching.
To start using caching possibilities you need pass cache
object. You have to
provide unique key
value and either a caching strategy
or shouldFetch
function.
- Cache
strategy
- use one of pre-defined caching strategies to defined at what state resource is valid or not:
api.actions.invoke({
method: 'GET',
headers: { Accept: 'application/json' },
endpoint: 'https://my-api.com/items/',
cache: {
key: 'GET/my-api.com/items',
strategy: api.cache.get(api.constants.CACHE_TYPES.TTL_SUCCESS).buildStrategy({
ttl: 600000, // 10 minutes
}),
},
});
shouldFetch
function - a custom function to defined when resource valid:
api.actions.invoke({
method: 'GET',
headers: { Accept: 'application/json' },
endpoint: 'https://my-api.com/items/',
cache: {
key: 'GET/my-api.com/items',
shouldFetch({ state: CachedApiState }) {
// Define your logic when the resource should be re-fetched
return true;
},
},
});
* Check getResult
selector docs for CachedApiState
structure.
If you're restoring redux state from offline storage, there might be some interrupted
fetch requests - which can restore your app in a broken state. You can
invalidate all the cached redux state, or selectively with cacheKey
.
dispatch(
api.actions.invalidateCache(
(cacheKey: ?string) // unique cache key
)
);
Clear all the cached redux state, or selectively with cacheKey
.
dispatch(
api.actions.clearCache(
(cacheKey: ?string) // unique cache key
)
);
Select all information about API request.
const response: ?CachedApiState = api.selectors.getResult(
(state: Object), // redux state
(cacheKey: string) // unique cache key
);
The selected CachedApiState
object has a structure of:
{
fetching: boolean, // is fetching in progress
fetched: boolean, // was any fetch completed
error: boolean, // was last response an error
timestamp: ?number, // last response timestamp
successPayload: ?any, // last success response payload
errorPayload: ?any, // last error response payload
}
* If getResult
response is undefined
it means the API request wasn't
initialized yet.
SIMPLE_SUCCESS
- uses previous successful fetch result
const strategy = api.cache.get(api.constants.CACHE_TYPES.SIMPLE_SUCCESS).buildStrategy();
SIMPLE
- uses any previous payload fetch result
const strategy = api.cache.get(api.constants.CACHE_TYPES.SIMPLE).buildStrategy();
TTL_SUCCESS
- uses previous successful fetch result if time to live (TTL) was not reached
const strategy = api.cache.get(api.constants.CACHE_TYPES.TTL_SUCCESS).buildStrategy({
ttl: 1000,
});
TTL
- uses any previous fetch result if TTL was not reached
const strategy = api.cache.get(api.constants.CACHE_TYPES.TTL).buildStrategy({
ttl: 1000,
});
- Crypto prices (source code) - cached API requests that sync with localStorage
- Codepen demo - GutHub user repository searcher
- Demos monorepo - various demos using
create-react-app
etc.
There are other solutions if redux-cached-api-middleware
doesn't fit your needs:
redux
redux-thunk
redux-api-middleware
redux-persist
- sync redux store with local (or any other) storage