Redux Actions in Depth

tl;dr

Actions are objects that are dispatched and tell reducers what to do. They contain a type - usually a string, and some payload - usually an object.

{
  type: 'DO_SOMETHING',
  payload: {
    // some data
  }
}

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Objects containing information that are dispatched to the reducers, which in turn update the store.

// An example Action object
{
  type: 'ADD_TODO', // 'type' is required
}
// An example Action object with a TYPE and PAYLOAD (i.e. data being sent to reducer, but we aren't calling it 'payload'. it'll still work..)
{
  type: 'ADD_TODO',
  text: 'Build my first Redux app'
}
// An example Action object with a TYPE and PAYLOAD (i.e. data being sent to reducer)
{
  type: 'ADD_TODO',
  payload: {
    text: 'Build my first Redux app',
    status: 'In Progress'
  }
}
// An example Action object with a TYPE (constant) and PAYLOAD (i.e. data being sent to reducer)
{
  type: ADD_TODO,
  payload: {
    text: 'Build my first Redux app',
    status: 'In Progress'
  }
}

Dispatching Actions

// Dispatch an Action (inline)
dispatch({
  type: "ADD_TODO",
  payload: {
    text: "Build my first Redux app",
    status: "In Progress"
  }
});
// Dispatch an Action (saved as an Action Creator function)
dispatch(addTodo("Build my first Redux app"));
Vocabulary Type
Action Object
Action type String recommended, could be anything serializable
Action (type) constant type saved as const
Action payload Object usually, could be anything
Action creator Function
Action: {
  type: String,
  payload: String/Object/Array/whatever
}

Type and Payload

On an Action object:

  • type is required. Every action must have a type. We use this type in our reducers to determine what changes to make for what type of action. Calling it type is convention in Redux, and a hard requirement in @reduxjs/toolkit, where you can reference action.type for a function created as a result of createAction()
  • payload is optional. It is the information being sent to the reducer, could be anything (sring, object, array whatever). Calling it payload is convention, you can call it whatever you want and reference it in your reducer by that name, but sticking to convention is good.

Action Type (String literal)

Every action must have a type property specifying what type of an action is being performed. The value should be a String (recommended).

// An example Action object
{
  type: 'ADD_TODO',
}

Action Type (Constant)

the action type strings saved as constants.

// Action constants (saving strings as const to avoid typos)
const ADD_TODO = "ADD_TODO";
// Combining action constants into one file and importing/exporting the relevant ones
import { ADD_TODO, REMOVE_TODO } from "../actionTypes";
// An example Action object with a TYPE and PAYLOAD (i.e. data being sent to reducer)
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Why?

to avoid typos, to gather them all in one place and to easily import/export them

Action Creators

Functions generating action objects (instead of writing action objects directly in the code at the time of dispatch).

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}
// Dispatch an Action (saved as an Action Creator function)
dispatch(addTodo("Build my first Redux app"));

Why?

Instead of creating the objects inline at the time of dispatch, you can save them before hand. The major benefit of doing that is that when you need to change an action object later, you’d only do it in one place instead of making inline changes where you dispatched that particular action

Bound Action Creator

an function that also includes dispatch() alongwith the action creator, i.e. it is bound to automatically dispatch when called:

const boundAddTodo = text => dispatch(addTodo(text));
const boundCompleteTodo = index => dispatch(completeTodo(index));

bindActionCreators() is there to automatically bind many action creators to a dispatch() function. Redux only.

createAction()

From the recommended and opinionated @reduxjs/toolkit, createAction() combines the process of creating an action type (constant) and an action creator (function) into a single step.

Funnily enough, both action constants and action creators are listed under reducing boilerplate, when in fact they are responsible for adding more lines of code and more files and folders.. The toolkit’s createAction() is the one that actually reduces code and complexity and makes Ducks (actions, reducers for one feature in one file) possible.

createAction() takes an action type as string and returns an action creator function for that action type. In other words, it creates an Action creator by taking an action type. Generated function takes a single argument that becomes action.payload. If you want to customize that payload, you can pass a second argument, i.e. the prepare() function to createAction().

import v4 from "uuid/v4";

const addTodo = createAction("todos/add", function prepare(text) {
  // the prepare() is optional, for when you need to customize the payload
  return {
    payload: {
      text,
      id: v4(),
      createdAt: new Date().toISOString()
    }
  };
});

Notice that in this example we’re also using uuid and Date() to add id and createdAt values to our payload. Following is the resulting Action object that will be created:

console.log(addTodo("Write more docs"));
{
  type: 'todos/add',
  payload: {
    text: 'Write more docs',
    id: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
    createdAt: '2019-10-03T07:53:36.581Z'
  }
}

If you don’t provide a prepare(), the single parameter passed to the generated action creator, (addTodo() in the example above) will become action.payload. For example:

const increment = createAction("counter/increment");

let action = increment(); // no arg, no payload
// { type: 'counter/increment' }

action = increment(3); // single parameter = action.payload
// returns { type: 'counter/increment', payload: 3 }

You can use toString() or type to reference the action type in a reducer. For example: addTodo.type

const increment = createAction("INCREMENT");
const decrement = createAction("DECREMENT");

function counter(state = 0, action) {
  switch (action.type) {
    case increment.type:
      return state + 1;
    case decrement.type:
      return state - 1;
    default:
      return state;
  }
}
createAction() API Returns
foo.type Action type (string)
foo.toString() Action type (string)
foo.payload Action payload (could be anything)
foo.match() determine if the passed action is of the same type as an action that would be created by the action creator

foo is the generated action creator..

Using information from Action objects in Reducer functions

A bit out of scope for this article, but i wanted to show where and how the type and payload will be used:

This is the Action we are dispatching (basic form, foregoing action constants and action creators at the moment)

// Dispatch an Action (inline)
dispatch({
  type: "ADD_TODO",
  payload: {
    text: "Build my first Redux app",
    status: "In Progress"
  }
});

And this is how we are using that dispatched information in the Reducer to update our Store

// An example Reducer function responsible for updating state
function todoApp(state = [], action) {
  switch (
    action.type // applying changes based on action 'type'
  ) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.payload.text, // using details from 'payload' that the action sent
          status: action.payload.status
        }
      ];

    default:
      return state;
  }
}

When using createAction() you will use addTodo.type instead of ADD_TODO in your CASE statements