Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

redux-interactions

Package Overview
Dependencies
Maintainers
3
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

redux-interactions

A streamlined approach to managing your Redux action creators and reducers.

  • 1.3.0
  • Source
  • npm
  • Socket score

Version published
Maintainers
3
Created
Source

redux-interactions

A streamlined approach to managing your Redux action creators and reducers.

For a TL;DR of what this package provides, you can skip down to Streamlining The Pattern.

If you wish to see it in practice, check out examples/todomvc-react. You can run it locally by checking out this repository and running npm run example.

A Pattern

From using Flux/Redux for a while, we've observed a few things:

  • Developers naturally think of action creators and reducers as a group of code (i.e. each action creator tends to be associated with a single piece of reducer logic).
  • When we make use of broad actions that are consumed by multiple reducers, it quickly becomes difficult to track/test/maintain all of the effects of that action.
  • A lot of business logic is necessarily asynchronous, which can't be encapsulated in a reducer.

These observations have landed us on a pattern where you group related action creators and reducers into a single file, which we call an interaction.

For example, a hypothetical, offline-only, todos app might have a "todos" interaction that manages the state of the todos being displayed to the user:

todos.js:

// Action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';

// Action Creators

export function add(id, text, completed = false) {
  return {type: ADD_TODO, id, text, completed};
}

export function toggle(id) {
  return {type: TOGGLE_TODO};
}

// Reducers

export function reducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return _reduceAdd(state, action);
    case TOGGLE_TODO:
      return _reduceToggle(state, action);
    default:
      return state;
  }
}

function _reduceAdd(state, action) {
  return [
    ...state,
    {id, text, completed},
  ];
}

function _reduceToggle(state, action) {
  return state.map(todo => {
    if (todo.id !== id) return todo;
    return {
      ...todo,
      completed: !todo.completed,
    };
  });
}

The store would wire it up by referencing todos.reducer, and components would trigger actions via a dispatch(todos.add()), etc. Hopefully a pretty straightforward pattern.

The interesting outcome of this approach is that it subtly changes your approach to managing business logic: Each action/reducer pair tends to encapsulate a single state modification, rather than an a swath of business logic. More complicated business logic naturally flows via thunked actions that complete all of our state modification actions/reducers.

For example, if we wanted to add a server to the mix, we might modify the above interaction to include another action creator:

todos.js:

export function addLocal(id, text, completed = false) {
  return {type: ADD_TODO, id, text, completed};
}

export function add(text, completed = false) {
  return async (dispatch, _getState) => {
    const todo = {id: uuid.v4(), text, completed, saving: true};
    // Optimisticaly add the todo to the store for immediate user feedback.
    dispatch(addLocal(todo));

    try {
      // Lets assume this succeeds for any 2xx; and we assume that means the
      // todo was successfully persisted.
      apiRequest('post', '/todos', {body: todo});
      dispatch(markSaved(todo.id));
    } catch (error) {
      // TERRIBLE user experience, but this is just an example.
      dispatch(flash.error(`Failed to save todo, please try again`));
      dispatch(removeLocal(todo.id));
    }
  };
}
…

This new action creator nicely encapsulates all business logic around adding a todo to the application. For ease of discussion, let's call it a business action (creator). And the more focused state modification actions like addLocal as state action (creators).

Business actions very rarely dispatch a raw action directly. Instead, they compose state actions and frequently perform asynchronous workflows.

Streamlining The Pattern

Now that we have that pattern in place, we can look at codifying it a bit more. Notice all the boilerplate surrounding state actions? Yeah, we can do better.

import { Interactions, reducer } from 'redux-interactions';

/**
 * The `Interactions` class helps you manage the boilerplate of a reducer that
 * dispatches to specific reducers per action type.
 *
 * It exposes a `reducer` method for you to bind into your store.
 *
 * Also, notice that we are immediately _instantiating_ and exporting an
 * instance of this class so that you can dispatch its actions in a
 * straightforward manner.  I.e. `dispatch(todos.toggle(id))`.
 *
 * `Interactions` also provides a bit of additional utility via its
 * constructor:
 *
 *   * All public methods are bound to the instance, guaranteeing that `this` is
 *     always correct regardless of how the method is called.
 *
 *   * The class is exposed as a property on the instance, to aid unit testing.
 *     For example, `todos.Todos`.
 *
 */
export default new class Todos extends Interactions {

  /**
   * Initial state for the subset of the store managed by these interactions.
   */
  initialState = [];

  /**
   * Add a todo, without involving the server.
   *
   * `@reducer` takes care of the boilerplate of state actions for you:
   *
   * 1. It generates a unique action type, based on the name of the interactions
   * class and method.  In this case: `TODOS:ADD_LOCAL`.
   *
   * 2. It generates an action creator by the same name, that packages up any
   * arguments passed to it.  In this case:
   *
   *   addLocal(...args) {
   *     return {type: 'TODOS:ADD_LOCAL', args};
   *   }
   *
   * 3. It registers the decorated function to be run when that action type is
   * encountered.  `state` is always the first argument, `action.args` are
   * expanded to be the rest of the arguments.
   */
  @reducer
  addLocal(state, id, text, completed = false) {
    return [
      ...state,
      {id, text, completed},
    ];
  }

  /**
   * Toggle a todo's completion state, locally.
   *
   * You can also specify a custom action type, if you like.
   *
   * Because `@reducer` generates types based on function names, which can be
   * mangled by uglify, the generated type is not human understandible in that
   * environment.  If you prefer to have your action types easily traceable,
   * even in production, consider passing explicit action types.
   */
  @reducer('CUSTOM_TODOS_TOGGLE')
  toggleLocal(state, id) {
    return state.map(todo => {
      if (todo.id !== id) return todo;
      return {
        ...todo,
        completed: !todo.completed,
      };
    });
  }

  /**
   * Add a todo and let the server know.
   *
   * This rounds off the example, in an effort to show off the pattern.  Note
   * that this method is purely an action creator; there's nothing special going
   * on here.
   */
  add(text, completed = false) {
    return async (dispatch, _getState) => {
      const todo = {id: uuid.v4(), text, completed, saving: true};
      // Optimisticaly add the todo to the store for immediate user feedback.
      dispatch(this.addLocal(todo));

      try {
        // Lets assume this succeeds for any 2xx; and we assume that means the
        // todo was successfully persisted.
        apiRequest('post', '/todos', {body: todo});
        dispatch(this.markSaved(todo.id));
      } catch (error) {
        // TERRIBLE user experience, but this is just an example.
        dispatch(flash.error(`Failed to save todo, please try again`));
        dispatch(this.removeLocal(todo.id));
      }
    };
  }

};

FAQs

Package last updated on 04 Apr 2016

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc