如何在Redux中实现缓存?

时间:2017-03-23 15:35:57

标签: javascript redux

如果我的商店中已有数据,我想避免两次调用API。

如何使用Redux执行此操作?

6 个答案:

答案 0 :(得分:9)

我假设您使用async actions来处理API调用。

这是我放置缓存逻辑的地方,这会产生一个很好的封装:

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)
              })
        }
    }
}

为本地存储实施isCachedgetCachedsetCached应该相当简单。

答案 1 :(得分:8)

我认为理想的解决方案是使用Reselect选择器(https://github.com/reactjs/reselect)。这是一个人为的例子:

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)
);

像这样使用:

import { getObj } from './selectors';

const ExampleComponent = ({obj}) => <div>{ obj.name }</div>;

const mapStateToProps = state => ({
  obj: getObj(state)
});

export default connect(mapStateToProps)(ExampleComponent);

首次运行时,将从所有obj的列表中“选择”基于某些id(也在州内)的一个objs。下一次,如果输入没有改变(查看等效定义的重新选择文档),它将只返回上次计算的值。

您还可以选择插入不同类型的缓存,例如LRU。这有点先进,但非常可行。

Reselect的主要优点是它允许您干净地优化而无需手动维护redux中的额外状态,如果对原始数据进行了更改,则必须记住更新。蒂莫的答案很好,但我认为它的缺点是它不会缓存昂贵的客户端计算(我知道这不是确切的问题,但这个答案是关于最佳实践的redux缓存,一般适用于你的问题),只取。你可以做一些与蒂莫建议的非常类似的事情,但是将重新选择纳入一个非常整洁的解决方案。在动作创建者中你可以有这样的东西:

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

  return fetchSomething()
    .then(data => {
      dispatch(receiveObj(data));
      return data;
    });
};

你可能有一个专门用于hasObj的选择器,可能基于上面的选择器(我这里专门用来展示如何轻松地组合选择器),如:

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

一旦你开始使用它与redux接口,即使对于简单的选择,在mapStateToProps中专门使用它也是有意义的,以便在将来的时间,如果计算状态的方式发生变化,你会做不需要修改使用该状态的组件的所有。这种情况的一个例子可能是,当用于在几个不同的组件中呈现列表时,有一个TODO数组。稍后在您的应用程序开发过程中,您意识到您希望默认情况下仅将那些TODO列表过滤为不完整的。您可以在一个地方更改选择器,然后就完成了。

答案 2 :(得分:4)

我提出了同样的问题,我想在我的动作和reducer之间添加一个缓存层。我的解决方案是创建一个中间件,在请求操作进入实际的thunk之前缓存它,该操作从API请求数据。

这样做有一个优点,您不需要修改现有的操作和减速器。你只需添加一个中间件。以下是中间件的外观:

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;

https://stackblitz.com/edit/redux-cache-middleware试用现场演示或查看我的博文,了解更多信息http://www.tonnguyen.com/2018/02/13/web-programming/implement-a-cache-middleware-for-redux/

答案 3 :(得分:2)

一种简单快捷的方法(虽然不推荐用于任何可扩展的):

使用redux-persist来保留(缓存)商店。每当它重新水化时,您就知道您之前存在的数据 - 请阅读链接中的文档,了解它的工作原理以及如何设置。

为避免远程服务器上出现不必要的数据提取,您可以将URL(as Timos answer)缓存到localStorage等,并在进行实际提取之前检查其是否存在。

动作:

    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 })
                   })
        }
    }

网址的简单自定义缓存:

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
    }
}

当/如果刷新了redux-persist数据或者使数据“变旧”的其他东西时,您还需要删除缓存的URL。

我建议使用带有持久性的redux存储来完成整个事情,而是在其上建立reducer / action逻辑。有很多方法可以做到这一点,我强烈建议您探索redux,redux-sagaredux-persist以及常见的概念/设计模式。

基本示例的旁注: 您还可以使用redux-persist-transform-expire transform for redux-persist让缓存的数据在某个时间点到期,并修改它以删除相关的缓存URL。

答案 4 :(得分:0)

我为此专门建立了一个库-redux-cached-api-middleware

一个示例用法,成功的响应将被缓存(从商店中重复使用)10分钟:

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.payload} />;
    if (result.payload) return <Items data={result.payload} />;
    return <div>No items</div>;
  }
}

ExampleApp.propTypes = {
  fetchData: PropTypes.func.isRequired,
  result: PropTypes.shape({}),
};

const enhance = connect(
  state => ({
    result: api.selectors.getResult(state, 'GET/my-api.com/items'),
  }),
  dispatch => ({
    fetchData() {
      return dispatch(
        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: 10 * 60 * 1000 }), // 10 minutes
          },
        })
      );
    },
  })
);

export default enhance(ExampleApp);

您可以传递缓存strategy或自定义shouldFetch函数来确定何时应重新获取资源(docs)。

该库使用redux-thunk(用于异步操作)和redux-api-middleware(用于调用API)作为对等方依赖项,并且设置非常简单:

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)
);

答案 5 :(得分:0)

不要重新发明缓存,只需利用HTTP缓存即可。
您的代码实际上应该不了解缓存机制。
只需在需要数据时发出http请求,无论是通过redux thunk,promise等,还是直接不带redux都没关系。
HTTP缓存将为您进行缓存。
当然,要使其正常工作,您需要正确配置服务器以设置适当的缓存参数和有效性。