1.0.2 • Published 6 years ago

redux-actioner v1.0.2

Weekly downloads
20
License
GPL-3.0
Repository
github
Last release
6 years ago

Redux Actioner

Make Redux actions simple, predictable, and maintainable.

Introduction

Creating actions should be easy and consistent. Inspired by Flux Standard Actions, this framework facilitates the implementation of Redux actions for the sake of simplicity, testing, and maintainability.

Installation

yarn add redux-actioner

Simplicity

Manage your actions like reducers. Redux Actioner provides createActionFactory which you can use to namespace your actions and add Action Slices that are broken down into three types: REQUEST, SUCCESS, and FAILURE.

{
  type: 'TODO/ADD/REQUEST',
  baseType: 'TODO/ADD',
  payload: {
    text: 'Do something great.'
  },
  actionType: 'REQUEST'
}

Here is our REQUEST action that is namespaced in the TODO factory > ADD slice. The baseType property contains the factory namespace and the slice name for the ease of handling in reducers.

Maintainability

Redux Actioner abstracts the frustration from creating objects and maintaining an application. Request action creators are reusable and easy to update when your API changes. Components remain pure of API versioning and integration with Redux Saga and Redux Observable is seamless.

Usage

Define Actions

import { createActionFactory, request } from 'redux-actioner';
const createSliceAction = createActionFactory('TODO');

export const ADD = createSliceAction('ADD', ({ text, title, listId: list_id })
  => ({ text, title, list_id }));

/*
ADD = {
  baseType: 'TODO/ADD',
  REQUEST: 'TODO/ADD/REQUEST',
  SUCCESS: 'TODO/ADD/SUCCESS',
  FAILURE: 'TODO/ADD/FAILURE',
  requestPayloadCreator: ({ text, title, listId: list_id })
    => ({ text, title, list_id }))
}
*/

In the example above we have created a namespace called "TODO" and added an action to it called "ADD." In the comment we can see the baseType is TODO/ADD which will make it easy to bind and reduce. The payload creator is included for creating request action types which makes for a predictable payload.

Bind to Dispatch

import { connect } from 'react-redux';
import { bindRequestActions } from 'redux-actioner';
import * as TodoActions from '../actions';

class TodoForm extends React.Component {
  state = { text: "Do something greatest.", title: "Tomorrow..." }

  onSubmit() {
    this.props.actions.addTodoItem({
      listId: this.props.listID,
      ...this.state.form
    });
  }

  render() {
    <Form onSubmit={() => this.onSubmit()} />
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    actions: bindRequestActions({
      addTodoItem: TodoActions.ADD
    }, dispatch);
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoForm);

The example above will feel familiar with the only difference being that Actioner does not require action creators in your components. ActionSlice objects are given to bindRequestActions which will return an object of action creators similar to Redux's bindActionCreators. This keeps components clean of API changes and minimizes redundancies.

Reduce and Slice

import * as TodoActions from '../actions';

function todoReducer(state = { items: [], isSubmitting: null }, action) {
  const { baseType } = action;

  switch(baseType) {
    case TodoActions.ADD.BASE_TYPE:
      return addSlice(state, action);
    case TodoActions.REMOVE.BASE_TYPE:
      return removeSlice(state, action);
  }
}

function addSlice(state, action) {
  const { actionType, payload } = action;
  switch (actionType ) {
    case 'REQUEST':
      return { ...state, isSubmitting: true };
    case 'SUCCESS':
      const newTodo = { createdAt: new Date(), ...payload };
      return { isSubmitting: false, items: state.items.concat(newTodo) };
    break;
  }
}

In the example above you will see our todoReducer which has been abstracted into slices to deal with ActionSlice types. When working with large reducers and many actions it becomes a necessity to break out into slices. The end result is cleaner and easier to manage.

Action Context

When managing side effects it can be difficult to keep track of data as it flows from action to action. Maintaining context is important to ensure actions that respond to requests can resolve later on when reducing.

Payload data from a request action can be abstracted and dispatched in success or failure actions by providing context to success(actionSlice, context) or failure(actionSlice, context):

const requestAction = request(TodoActions.ADD)({ text: "Do something most great."});
const { payload } = requestAction;
const { text } = payload;
fetch(apiRequest)
  .then(response =>
    const successAction = success(TodoActions.ADD, { text })(response);
    store.dispatch(successAction);
  )
  .catch(error =>
    const errorAction = failure(TodoActions.ADD, { text })(error);
    store.dispatch(errorAction);
  );

Testing

By using standardized payload creators, action data becomes more predictable and easier to test.

The payload to a REQUEST action is reduced through a function to prepare the payload parameters for dispatch. It makes API changes flexible and easy to manage in action creators.

describe("TODO/ADD request action", () => {
  const payload = { text: 'Do something greater.', title: 'Today...', listId: 1 };

  it("converts from camel case to snake case", () => {
    expect(request(TodoActions.ADD)(payload))
      .toEqual(jasmine.objectContaining({
        text: payload.text,
        title: payload.title,
        list_id: payload.listId
      }));
  }
})

Manual

Contributors