web-dev-qa-db-fra.com

Comment implémenter la mise en cache dans Redux?

J'aimerais éviter d'appeler une API deux fois si j'ai déjà les données dans mon magasin. 

Comment puis-je faire cela avec Redux? 

18
ng2user

Je suppose que vous utilisez actions asynchrones pour gérer vos appels d'API.

C'est l'endroit où je mettrais la logique de mise en cache, ce qui résulte en une encapsulation de Nice:

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

L'implémentation de isCached, getCached et setCached pour votre stockage local devrait être plutôt simple.

8
Timo

À mon avis, la solution idéale consiste à utiliser des sélecteurs Reselect ( https://github.com/reactjs/reselect ). Voici un exemple artificiel:

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

Utilisé comme ceci:

import { getObj } from './selectors';

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

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

export default connect(mapStateToProps)(ExampleComponent);

La première fois que vous exécutez cette opération, une obj basée sur une id (également dans l'état) sera "sélectionnée" dans la liste de toutes les objs. La prochaine fois, si les entrées n’ont pas changé (consultez la section Nouvelle sélection de la documentation pour la définition de l’équivalence), la valeur calculée de la dernière fois sera simplement renvoyée.

Vous avez également la possibilité de brancher un type de cache différent, par exemple. LRU. C'est un peu plus avancé, mais très faisable.

Le principal avantage de Reselect réside dans le fait qu’elle vous permet d’optimiser proprement sans conserver manuellement un état supplémentaire dans redux que vous devrez alors ne pas oublier de mettre à jour si une modification des données d’origine était effectuée. La réponse de Timo est bonne, mais je dirais que la faiblesse réside dans le fait qu’il ne cache pas les calculs coûteux effectués par le client (je sais que ce n’était pas la question exacte, mais cette réponse concerne les meilleures pratiques de mise en cache redux en général, appliquées à votre problème. ), seulement chercher. Vous pouvez faire quelque chose de très similaire à ce que Timo suggère, mais incorporer resélectionner pour une solution très soignée. Dans un créateur d'action, vous pouvez avoir quelque chose comme ceci:

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

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

Vous auriez un sélecteur spécifiquement pour hasObj potentiellement basé sur le sélecteur ci-dessus (je le fais ici spécifiquement pour montrer comment vous pouvez composer facilement des sélecteurs), comme:

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

Une fois que vous commencez à utiliser ceci pour interfacer avec redux, il devient logique de l’utiliser exclusivement dans mapStateToProps, même pour des sélections simples, de sorte qu’à l'avenir, si la façon dont cet état est calculé change, vous n'avez pas besoin de modifier all. des composants qui utilisent cet état. Un exemple de cela pourrait être quelque chose comme avoir un tableau de TODO quand est utilisé pour rendre une liste dans plusieurs composants différents. Plus tard dans votre processus de développement d'applications, vous réalisez que vous souhaitez filtrer cette liste de tâches à effectuer par défaut pour ne conserver que celles qui sont incomplètes. Vous changez le sélecteur à un endroit et vous avez terminé.

7
dpwrussell

Je suis venu avec le même problème, où je voulais ajouter une couche de cache entre mon action et réducteur. Ma solution consistait à créer un middleware, à mettre en cache l'action Request avant qu'elle ne soit transmise au thunk réel, qui demande des données à l'API.

Faire cela a un avantage que vous n'avez pas besoin de modifier votre action et votre réducteur existants. Vous venez d'ajouter un middleware. Voici à quoi ressemble le middleware:

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;

Essayez la démo en direct sur https://stackblitz.com/edit/redux-cache-middleware ou consultez mon billet de blog pour plus d'informations http://www.tonnguyen.com/2018/02/ 13/programmation-web/implémenter-un-cache-middleware-pour-redux/

4
Ton Nguyen

Un moyen simple et rapide de le faire (bien que cela ne soit pas recommandé pour quelque chose de modulable):

Utilisez redux-persist pour persister (mettre en cache) le magasin. Chaque fois qu'il se réhydrate, vous savez que les données que vous aviez précédemment sont présentes - lisez la documentation dans le lien pour en savoir plus sur son fonctionnement et sa configuration.

Pour éviter des extractions de données peu minutieuses sur le serveur distant, vous pouvez mettre en cache les URL ( lorsque Timos répond ) à localStorage ou autre, et simplement en vérifier l'existence avant de procéder à l'extraction proprement dite.

Action:

    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 cache personnalisé pour les URL:

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

Vous devrez également supprimer l'URL en cache lorsque/si les données de redux-persist sont vidées ou quelque chose d'autre qui rend les données "anciennes". 

Je recommande de faire le tout en utilisant le magasin redux avec persisting, et de modéliser plutôt la logique réducteur/action. Il y a plusieurs façons de le faire, et je recommande fortement d'explorer redux, redux-saga et redux-persist et des concepts/modèles de conception courants.

Sidenote sur l'exemple de base: Vous pouvez également utiliser redux-persist-transformation-expire transformer pour redux-persist afin de laisser les données en cache expirer à un moment donné, et les modifier pour supprimer les informations pertinentes. url caché en le faisant.

2
Oyvind Andersson

J'ai construit une bibliothèque spécialement pour cela - redux-cached-api-middleware .

Un exemple d'utilisation dans lequel une réponse réussie serait mise en cache (réutilisée à partir du magasin) pendant 10 minutes:

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

Vous pouvez passer une strategy de mise en cache ou votre fonction shouldFetch personnalisée pour déterminer le moment où la ressource doit être extraite à nouveau ( docs ).

La bibliothèque utilise redux-thunk (pour les actions asynchrones) et redux-api-middleware (pour l'appel des API) en tant que dépendance entre homologues, et la configuration est assez simple:

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

Ne réinventez pas la mise en cache, utilisez simplement le cache HTTP.
Votre code ne devrait pratiquement pas connaître le mécanisme de mise en cache.
Faites simplement la requête http lorsque vous avez besoin des données, que ce soit par redux thunks, promesses, etc. ou directement sans redux.
Le cache HTTP fera la mise en cache pour vous.
Bien sûr, pour que cela fonctionne, vous devez configurer correctement votre serveur afin de définir les paramètres de mise en cache et la validité appropriés.

0
pakman