README.md

Introduction to the project the way it is presented on GitHub

Redux-optimum

Redux-optimum is a library which aims is to make UI optimistic data fetching incredibly easy. Give it some endpoints (and all the settings they need) and it will provide you with the redux actions you might want for each stage during the process of calling an API. Furthermore, redux-optimum provides you with an API queue manager. You can state wether or not you want to retry a call, how many times and when, or if you simply want to revert the data upon failure and abandon it. The library also allows you to centralize in one place all the definitions of these API calls and then worry about implementing the reducers any way you normally would. The queue manager has its own key in the store and provides you with all the info you might need so you can see what requests are being processed, if there are any errors, as well as any countdown till the next retry. Thus, allowing you to provide helpful feedback to your users.

The library offers a credential management system where you can, upon an API call failure, decide to refresh a token (using any HTTP code you might wish). You can also queue up all calls to certain endpoints until the user logs in.

This library is built on top of the redux-saga redux middleware. This kind of complex data flow management couldn't be done without it. It takes the whole idea behind most common uses of any redux middleware such as redux-saga or redux-thunk and aims at providing an opinionated and out-of-the-box solution for an easy and centralized API call management system without locking you in when you have edge-cases.

Getting started

Install

$ npm install redux-optimum

or

Usage Example

You want to call an endpoint and have your global store be notified the call has started, or has been placed into a queue (one queue per action type) and is waiting to be processed? You want the call to notify the store that it has been processed successfully or is still in the queue because it has failed? You want to decide for which HTTP codes, we deem the call to be a true failure, for which we place them back in the queue to retry them, or for which, we try to refresh the credentials?

Wouldn't that be great to have all that with minimum configuration? Well, you can. Centralize all your definitions in one config file.

const config = {
    //Add default for every operations.
    operations : [
      {
        actionType: "SIMPLE_REQUEST_SUCCESS",
        APICallSettings: {
          endpoint: (originalActionPayload, store) => ApiEndpoints['SIMPLE_REQUEST_200'],
          method: 'get',
          requestParameters: {
            headers: {"Content-Type": "application/json"}, //used for logged in //Content-Type
            credentials: 'include', //cookie credential optional
            mode: 'cors'
          },
          payload: (originalActionPayload, store) => //(
            {
              //object_id: '98',
              //phone_number: "tresfsdf",
            }
          //),
        },
        needToBeLoggedIn: false,
        sendAccessToken: false, //none, header, query, body
        mode: "every", // or latest, -> just take latest call or queue all calls
        HTTPCodesRefreshToken: [-1], // List of HTTP codes, -1 for browser
        // failures
        HTTPCodeFailures: [],
        retriesDelays: [10, 30, 60, 180, 300], //default // empty no retries
        // single value means fix interval without interruption
        //queueUpRequests: true, //Queue up the request or replace them with
        // the latest -> invalidated by mode
        clearAfterAllRetriesFailed: false,
        stages: {
          begin: {
            actionType: "SIMPLE_REQUEST_SUCCESS_BEGIN",
            payload: (payload,
                      storeWhenDispatching,
                      operation) => (payload)
          },
          success: {
            actionType: "SIMPLE_REQUEST_SUCCESS_SUCCESS",
            payload: (response,
                      originalActionPayload,
                      storeWhenDispatching,
                      operation) => {}
          },
          failure: {
            actionType: "SIMPLE_REQUEST_SUCCESS_FAILURE",
            payload:  (error,
                       originalActionPayload,
                       storeWhenDispatching,
                       operation) => ({test:"test"} ),
          },
        },

    }

   ],
  credentialManagement: {
      loggedInSelector: (state) => true,
      getAccessToken: (store) => {}, //return tok in obj
      getRefreshToken: (store) => ({Authorization: 1234}), //return tok in obj
      refreshingTokenCallDetails: (refreshToken) => ({
        endpoint: ApiEndpoints['Token/REFRESH'],
        method: "get",
        HTTPCodeFailures: [],
        retriesDelays: [10, 30, 60, 180, 300], //default // empty no retries
        //requestPayload: JSON.stringify({send_tok: token}),
        requestParameters: {
          headers: {"Content-Type": "application/json", ...refreshToken}, //used
          // for logged in
          // Content-Type
          mode: 'cors'
        },
      }), // => make api call //Depending on mode, it sends the token with key
      // -> value
      uponReceivingFreshToken: (body) => {}, //to store token
    }
}

First, we will try to understand the generic settings for any operation. These are limited and concern only the credential Management:

config.js

credentialManagement: {
    loggedInSelector: (state) => true,
    providingAccessToken: (store) => {}, //Return obj, key value
    sendingRefreshToken: (store) => {}, // => make api call
    uponReceivingFreshToken: () => {}, //to store token
  }
loggedInSelector: (state) => aBoolean

The loggedInSelector method allows for a selector to be given to the queue manager. The queue manager expects the method to return a boolean and it defaults to true if not defined. The whole store is passed to it so you can extract this information easily if you already have it in the store. The idea behind it is to let you decide how you consider a user logged in and to give the queue manager this information.

getAccessToken: (store) => {propertyName: aString}, //Return obj, key value

getAccessToken makes you responsible for returning the access token in a dictionary form. It can be grabbed from the store, or from a global storage place (local storage or cookie). It expects the token to be a string and will use the property name, however it is used in the operation, whether within a header, a json, or a query parameter.

sendingRefreshToken: (store) => ({refreshTok: 1234}) //make api call

sendingRefreshToken behaves exactly the same way as getAccessToken. The only difference is that it will be processed not at the operation level but in the refreshingTokenCallDetails.

refreshingTokenCallDetails: () => ({
    endpoint: ApiEndpoints['Token/REFRESH'],
    method: "post",
    HTTPCodeFailures: [],
    retriesDelays: [10, 30, 60, 180, 300], //default // empty no retries
    //requestPayload: JSON.stringify({send_tok: token}),
    requestParameters: {
      headers: {"Content-Type": "application/json"}, //used for logged in //Content-Type
      mode: 'cors'
    },
    }) //Return obj, key value

uponReceivingToken.

uponReceivingToken: () => {}, //Return obj, key value

uponReceivingToken.

operations : [
      {
        actionType: "TEST",
        APICallSettings: {
          endpoint: (originalActionPayload, store) => ApiEndpoints['Company/TEST'],
          method: 'get',
          requestParameters: {
            headers: {"Content-Type": "application/json"}, //used for logged in //Content-Type
            credentials: 'include', //cookie credential optional
            mode: 'cors'
          },
          payload: (originalActionPayload, store) => //(
            {
              //object_id: '98',
              //phone_number: "tresfsdf",
            }
          //),
        },
        needToBeLoggedIn: false,
        sendAccessToken: false, //none, header, query, body
        mode: "every", // or latest,
        refreshToken: [], // List of codes
        HTTPCodeFailures: [],
        retriesDelays: [10, 30, 60, 180, 300], //default // empty no retries
        queueUpRequests: true, //Queue up the request or replace them with
        // the latest
        stages: {
          begin: {
            actionType: "TEST_BEGIN",
            payload: (payload,
                      storeWhenDispatching,
                      operation) => (payload)
          },
          success: {
            actionType: "TEST_SUCCESS",
            payload: (response,
                      originalActionPayload,
                      storeWhenDispatching,
                      operation) => {}
          },
          failure: {
            actionType: "TEST_FAILURE",
            payload:  (error,
                       originalActionPayload,
                       storeWhenDispatching,
                       operation) => ({...company, error: [...company.error, {[endpoint]: error}]} ),
          },
        },

    }

 ],

Here you define all the parameters for each operation.

actionType: string, required Name of the first action to be dispatched to trigger the API Call

APICallSettings: object Contain all the settings required to complete the appropriate API call.

createStore.js

import 'regenerator-runtime/runtime';
import test from './test'
import {
  combineReducersOptimistic,
  createStoreOptimistic} from 'redux-optimistic/dist/es'

import config from "../optimisticrc"


const appReducer = combineReducersOptimistic({
  test
});


const store = createStoreOptimistic(appReducer, config)


export {store}

Documentation

Translation

# Building examples from sources

$ git clone https://github.com/solalgaillard/redux-optimistic.git
$ cd redux-optimum

Below are the examples ported (so far) from the Redux repos.

Simple Flow

Under Construction

$ npm run simple-flow

# run unit test for simple-flow
$ npm run test-simple-flow

Complex Flow

Soon to come

With Persistence

Soon to come

License

Copyright (c) 2020 Solal Gaillard.

Licensed under The MIT License (MIT).

Last updated