Skip to content

karolis-sh/redux-cached-api-middleware

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

redux-cached-api-middleware

npm version Node.js CI License: MIT gzip size code style: prettier

Redux module that makes working with APIs a breeze.

Table of Contents

Why

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.

Installation

  1. 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.

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

Example

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

API

API Config

DEFAULT_INVOKE_OPTIONS

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.

DEFAULT_CACHE_STRATEGY

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

Redux Actions

invoke()

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.

invalidateCache()

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

clearCache()

Clear all the cached redux state, or selectively with cacheKey.

dispatch(
  api.actions.clearCache(
    (cacheKey: ?string) // unique cache key
  )
);

Redux Selectors

getResult()

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.

Caching Strategies

  • 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,
});

Demos

Other Solutions

There are other solutions if redux-cached-api-middleware doesn't fit your needs:

References