Home

Awesome

redux-logic

"One place for all your business logic and action side effects"

Redux middleware that can:

Build Status Known Vulnerabilities NPM Version Badge

tl;dr

With redux-logic, you have the freedom to write your logic in your favorite JS style:

Use the type of code you and your team are comfortable and experienced with.

Leverage powerful declarative features by simply setting properties:

Testing your logic is straight forward and simple. redux-logic-test provides additional utilities to make testing a breeze.

With simple code your logic can:

Redux-logic makes it easy to use code that is split into bundles, so you can dynamically load logic right along with your split UI.

Server rendering is simplified with redux-logic since it lets you know when all your async fetching is complete without manual tracking.

Inspired by redux-observable epics, redux-saga, and custom redux middleware, redux-logic combines ideas of each into a simple easy to use API.

Quick Example

This is an example of logic which will listen for actions of type FETCH_POLLS and it will perform ajax request to fetch data for which it dispatches the results (or error) on completion. It supports cancellation by allowing anything to send an action of type CANCEL_FETCH_POLLS. It also uses take latest feature that if additional FETCH_POLLS actions come in before this completes, it will ignore the outdated requests.

The developer can just declare the type filtering, cancellation, and take latest behavior, no code needs to be written for that. That leaves the developer to focus on the real business requirements which are invoked in the process hook.

import { createLogic } from 'redux-logic';

const fetchPollsLogic = createLogic({
  // declarative built-in functionality wraps your code
  type: FETCH_POLLS, // only apply this logic to this type
  cancelType: CANCEL_FETCH_POLLS, // cancel on this type
  latest: true, // only take latest

  // your code here, hook into one or more of these execution
  // phases: validate, transform, and/or process
  process({ getState, action }, dispatch, done) {
    axios
      .get('https://survey.codewinds.com/polls')
      .then((resp) => resp.data.polls)
      .then((polls) => dispatch({ type: FETCH_POLLS_SUCCESS, payload: polls }))
      .catch((err) => {
        console.error(err); // log since could be render err
        dispatch({ type: FETCH_POLLS_FAILED, payload: err, error: true });
      })
      .then(() => done()); // call done when finished dispatching
  }
});

Since redux-logic gives you the freedom to use your favorite style of JS code (callbacks, promises, async/await, observables), it supports many features to make that easier, explained in more detail

Table of contents

Updates

Full release notes of breaking and notable changes are available in releases. This project follows semantic versioning.

A few recent changes that are noteworthy:

v2.0.0

Updated to RxJS@6. Your logic code can continue to use RxJS@5 until you are ready to upgrade to 6.

Optimizations to reduce the stack used, especially if a subset of features is used.

v1.0.0

Transpilation switched to Babel 7 and Webpack 4

v0.12

These changes are not breaking but they are noteworthy since they prepare for the next version which will be breaking mainly to remove the single dispatch version of process hook which has been a source of confusion.

Goals

Usage

redux-logic uses rxjs@6 under the covers and to prevent multiple copies (of different versions) from being installed, it is recommended to install rxjs first before redux-logic. That way you can use the same copy of rxjs elsewhere.

If you are never using rxjs outside of redux-logic and don't plan to use Observables directly in your logic then you can skip the rxjs install and it will be installed as a redux-logic dependency. However if you think you might use Observables directly in the future (possibly creating Observables in your logic), it is still recommended to install rxjs separately first just to help ensure that only one copy will be in the project.

The rxjs install below npm install rxjs@^6 installs the lastest 6.x.x version of rxjs.

npm install rxjs@^6 --save  # optional see note above
npm install redux-logic --save
// in configureStore.js
import { createLogicMiddleware } from 'redux-logic';
import rootReducer from './rootReducer';
import arrLogic from './logic';

const deps = { // optional injected dependencies for logic
  // anything you need to have available in your logic
  A_SECRET_KEY: 'dsfjsdkfjsdlfjls',
  firebase: firebaseInstance
};

const logicMiddleware = createLogicMiddleware(arrLogic, deps);

const middleware = applyMiddleware(
  logicMiddleware
);

const enhancer = middleware; // could compose in dev tools too

export default function configureStore() {
  const store = createStore(rootReducer, enhancer);
  return store;
}


// in logic.js - combines logic from across many files, just
// a simple array of logic to be used for this app
export default [
 ...todoLogic,
 ...pollsLogic
];


// in polls/logic.js
import { createLogic } from 'redux-logic';

const validationLogic = createLogic({
  type: ADD_USER,
  validate({ getState, action }, allow, reject) {
    const user = action.payload;
    if (!getState().users[user.id]) { // can also hit server to check
      allow(action);
    } else {
      reject({ type: USER_EXISTS_ERROR, payload: user, error: true })
    }
  }
});

const addUniqueId = createLogic({
  type: '*',
  transform({ getState, action }, next) {
    // add unique tid to action.meta of every action
    const existingMeta = action.meta || {};
    const meta = {
      ...existingMeta,
      tid: shortid.generate()
    },
    next({
      ...action,
      meta
    });
  }
});

const fetchPollsLogic = createLogic({
  type: FETCH_POLLS, // only apply this logic to this type
  cancelType: CANCEL_FETCH_POLLS, // cancel on this type
  latest: true, // only take latest
  process({ getState, action }, dispatch, done) {
    axios.get('https://survey.codewinds.com/polls')
      .then(resp => resp.data.polls)
      .then(polls => dispatch({ type: FETCH_POLLS_SUCCESS,
                                payload: polls }))
      .catch(err => {
             console.error(err); // log since could be render err
             dispatch({ type: FETCH_POLLS_FAILED, payload: err,
                        error: true })
      })
      .then(() => done());
  }
});

// pollsLogic
export default [
  validationLogic,
  addUniqueId,
  fetchPollsLogic
];

processOptions introduced for redux-logic@0.8.2 allowing for even more streamlined code

processOptions has these new properties which affect the process hook behavior:

The successType and failType would enable clean code, where you can simply return a promise or observable that resolves to the payload and rejects on error. The resulting code doesn't have to deal with dispatch and actions directly.

import { createLogic } from 'redux-logic';

const fetchPollsLogic = createLogic({
  // declarative built-in functionality wraps your code
  type: FETCH_POLLS, // only apply this logic to this type
  cancelType: CANCEL_FETCH_POLLS, // cancel on this type
  latest: true, // only take latest

  processOptions: {
    // optional since the default is true when dispatch is omitted from
    // the process fn signature
    dispatchReturn: true, // use returned/resolved value(s) for dispatching
    // provide action types or action creator functions to be used
    // with the resolved/rejected values from promise/observable returned
    successType: FETCH_POLLS_SUCCESS, // dispatch this success act type
    failType: FETCH_POLLS_FAILED // dispatch this failed action type
  },

  // Omitting dispatch from the signature below makes the default for
  // dispatchReturn true allowing you to simply return obj, promise, obs
  // not needing to use dispatch directly
  process({ getState, action }) {
    return axios.get('https://survey.codewinds.com/polls').then((resp) => resp.data.polls);
  }
});

This is pretty nice leaving us with mainly our business logic code that could be easily extracted and called from here.

Full API

See the docs for the full api

Examples

Live examples

Full examples

https://github.com/jeffbski/redux-logic-examples/tree/master/examples/search-async-fetch

Comparison summaries

Following are just short summaries to compare redux-logic to other approaches.

For a more detailed comparison with examples, see by article in docs, Where do I put my business logic in a React-Redux application?.

Compared to fat action creators

Compared to redux-thunk

Compared to redux-observable

Compared to redux-saga

Compared to custom redux middleware

Implementing SAM/PAL Pattern

The SAM (State-Action-Model) pattern is a pattern introduced by Jean-Jacques Dubray. Also known as the PAL (proposer, acceptor, learner) pattern based on Paxos terminology.

A few of the challenging parts of implementing this with a React-Redux application are:

  1. where to perform the accept (interception) of the proposed action performing validation, verification, authentication against the current model state. Based on the current state, it might be appropriate to modify the action, dispatch a different action, or simply suppress the action.
  2. how to trigger actions based on the state after the model has finished updating, referred to as the NAP (next-action-predicate).

Custom Redux middleware can be introduced to perform this logic, but you'll be implementing most everything on your own.

With redux-logic you can implement the SAM / PAL pattern easily in your React/Redux apps.

Namely you can separate out your business logic from your action creators and reducers keeping them thin. redux-logic provides a nice place to accept, reject, and transform actions before your reducers are run. You have access to the full state to make decisions and you can trigger actions based on the updated state as well.

Solving those SAM challenges previously identified using redux-logic:

  1. perform acceptance in redux-logic validate hooks, you have access to the full state (model) of the app to make decisions. You can perform synchronous or asynchronous logic to determine whether to accept the action and you may augment, modify, substitute actions, or suppress as desired.
  2. Perform NAP processing in redux-logic process hooks. The process hook runs after the actions have been sent down to the reducers so you have access to the full model (state) after the updates where you can make decisions and dispatch additional actions based on the updated state.

<a name="other"></a>

Inspiration

redux-logic was inspired from these projects:

Minimized/gzipped size with all deps

(redux-logic only includes the modules of RxJS 6 that it uses)

redux-logic.min.js.gz 18KB

Note: If you are already including RxJS 6 into your project then the resulting delta will be much smaller.

TODO

Get involved

If you have input or ideas or would like to get involved, you may:

Supporters

This project is supported by CodeWinds Training

<a name="license"/>

License - MIT