How to implement caching in Redux?

I would like to avoid calling the API twice if I already have data in my store.

How do I do this using Redux?

+8
javascript redux
source share
4 answers

I assume that you are using asynchronous actions to handle API calls.

This is the place where I would put the caching logic, which leads to nice encapsulation:

export function fetchData(url) { return function (dispatch) { dispatch(requestData(url)) if (isCached(url)) { const cachedData = getCached(url) dispatch(receiveData(url, cachedData)) } else { return fetch(url) .then(response => response.json()) .then(json => { dispatch(receiveData(url, json)) setCached(url, json) }) } } } 

Implementing isCached , getCached and setCached for your local storage should be fairly simple.

+7
source share

The ideal solution for this, in my opinion, is to use Reselect selectors ( https://github.com/reactjs/reselect ). Here is a contrived example:

 import { createSelector } from 'reselect'; const getObjs = state => state.objs; const currentObjId = state => state.currentObjId; export const getObj = createSelector( [ currentObjId, getObjs ], (id, objs) => objs.get(href) ); 

Used as follows:

 import { getObj } from './selectors'; const ExampleComponent = ({obj}) => <div>{ obj.name }</div>; const mapStateToProps = state => ({ obj: getObj(state) }); export default connect(mapStateToProps)(ExampleComponent); 

The first time you run this, one of obj based on some id (also in state) will be "selected" from the list of all objs . Next time, if the inputs have not changed (look at re-fetching the documentation to determine equivalence), it will simply return the calculated value from the last time.

You also have the option of connecting a different type of cache, for example. LRU. It is a bit more advanced but very doable.

The main advantage of Reselect is that it allows you to purely optimize without manually saving the additional state in the redux, which you would need to remember if you changed the original data. Timo's answer is good, but I would say that the weakness is that it does not cache expensive client-side computing (I know this is not an exact question, but this answer concerns the best use of caching in the general case, applicable to your problem), only sampling. You can do something very similar to what Timo offers, but enable re-selection for a very neat solution. In the action creator, you can have something like this:

 export const fetchObj = (dispatch, getState) => { if (hasObj(getState())) { return Promise.resolve(); } return fetchSomething() .then(data => { dispatch(receiveObj(data)); return data; }); }; 

You would have a selector specifically for hasObj , potentially based on the above selector (here, I specifically show here how easy it is to group selectors), for example:

 export const hasObj = createSelector( [ getObj ], obj => !!obj ); 

As soon as you start using this for interacting with redux, it makes sense to use it exclusively in mapStateToProps even for simple samples, so that in the future, if the method of calculating this state has changed, you do not need to change all the components that use this state. An example of this might be something like a TODO array when used to render a list in several different components. Later in the application development process, you realize that you want to filter this TODO list by default only for incomplete ones. You change the selector in one place, and you're done.

+7
source share

I had the same problem where I wanted to add a cache layer between my action and the reducer. My solution was to create middleware to cache the Request action before it goes to the actual thunk that is requesting data from the API.

This has cons that you don’t need to modify an existing action and gear. You just add middleware. Here's what the middleware looks like:

 const cache = store => next => action => { // handle FETCH action only if (action.type !== 'FETCH') { return next(action); } // check if cache is available const data = window['__data']; if (!data) { // forward the call to live middleware return next(action); } return store.dispatch({ type: 'RECEIVE', payload: { data: `${data} (from cache)` } }); } export default cache; 

Try the live demo https://stackblitz.com/edit/redux-cache-middleware or check out my blog post for more information http://www.tonnguyen.com/2018/02/13/web-programming / implement-a-cache-middleware-for-redux /

+3
source share

A simple and quick way to do this (although not recommended for anything scalable):

Use redux-persist to save (cache) storage. Whenever it is rehydrated, you know that the data that you previously attended is to read the documents in the link, how it works and how to set it up.

To avoid unnecessary data collections on the remote server, you can cache URLs ( like Timos's answer ) to localStorage or such, and just check its existence before actually fetching.

Act:

  function fetchUsers(url){ if(isCached(url)) { // The data is already in the store, thanks to redux-persist. // You can select it in the state as usual. dispatch({ type: 'IS_CACHED', payload: url }) } else { return fetch(url) .json() .then((response) => { dispatch({ type: 'USERS_FETCH_SUCCESS', payload: response.data }) setCached(url) }) .catch((error) => { dispatch({ type: 'USERS_FETCH_FAILED', payload: error }) }) } } 

Simple custom cache for urls:

 const CACHE_NAME = 'MY_URL_CACHE' export function getUrlCache() { var localStorage try { localStorage = window.localStorage // Get the url cache from the localstorage, which is an array return ( localStorage.getItem(CACHE_NAME) ? JSON.parse(localStorage.getItem(CACHE_NAME)) : [] ) } catch(e){ // Make your own then... throw "localStorage does not exist yo" } } export function isCached(url) { var urlCache = getUrlCache() return urlCache.indexOf(url) !== -1 } export function setCached(url){ if( isCached(url) ) return false var urlCache = getUrlCache() urlCache.push(url) localStorage.setItem(CACHE_NAME, urlCache) return true } export function removeCached(url){ var myCache = getUrlCache() var index = myCache.indexOf(url) if(index !== -1){ myCache = myCache.splice(index, 1) localStorage.setItem(CACHE_NAME, urlCache) return true } else { return false } } 

You will also need to remove the cached url if / if the data with the abbreviation-persist are flushed or some other thing that makes the data "old".

I recommend doing all this using the redux repository, with preservation and, rather, simulating the reducer / action logic on it. There are many ways to do this, and I highly recommend learning about reductions, redux-saga and redux-persist, and general design concepts / patterns.

Sidenote on a basic example: You can also use a redux-persist-transform-expire transformer to reduce-persist so that the cached data expires at some point in time and modify it to remove the corresponding cached url.

+1
source share

All Articles