Idiomatic Redux: Why use action creators?

This is a post in the Idiomatic Redux series.


First in an occasional series of thoughts on good usage patterns for Redux

Preface 🔗︎

I've spent a lot of time discussing Redux usage patterns online, whether it be helping answer questions from learners in the Reactiflux channels, debating possible changes to the Redux library APIs on Github, or discussing various aspects of Redux in comment threads on Reddit and HN. Over time, I've developed my own opinions about what constitutes good, idiomatic Redux code, and I'd like to start sharing some of those thoughts. Despite my status as a Redux maintainer, these are just opinions, but I think they're pretty good approaches to follow :)

Action Creators 🔗︎

One of the most common complaints about Redux is the amount of "boilerplate" involved. People complain about having to declare constants, writing indirect code, touching multiple files to implement features, and so on. I've specifically seen people asking why "action creators" are needed in several places (such as here, here, and here).

So, why not just put all your logic right into a component? Dan Abramov has written some fantastic answers on similar topics, and I've answered this question several times, but I'd like to cover the topic in more detail.

First, let's start with some clarification of terms and concepts:

  • An action is a plain simple object, like {type : "ADD_TODO", text : "Buy milk"}.
  • An action type is the value for the type field in an action. Per the Redux FAQ, this field should be a string, although Redux only enforces that a type field exists in the action.
  • An action creator is a function that returns an action, like:
function addTodo(text) {
    return {
        type : "ADD_TODO",
        text
    }
}
  • A thunk action creator is a function that returns a function. If you're using the redux-thunk middleware, the inner function will be called and given references to dispatch and getState, like this:
function makeAjaxCall(someValue) {
    return (dispatch, getState) => {
        dispatch({type : "REQUEST_STARTED"});
        
        myAjaxLib.post("/someEndpoint", {data : someValue})
            .then(response => dispatch({type : "REQUEST_SUCCEEDED", payload : response})
            .catch(error => dispatch({type : "REQUEST_FAILED", error : error});    
    };
}

Reasons for Using Action Creators 🔗︎

Per Dan's answers on Stack Overflow, it's completely possible to do the work of making AJAX calls and calling dispatch entirely inline in a component. However, as programmers, it is good practice to encapsulate behavior, separate concerns, and keep code duplication to a minimum. We'd also like to keep our code as testable as possible.

To me, there are five primary reasons to use action creators rather than putting all your logic directly into a component:

  1. Basic abstraction: Rather than writing action type strings in every component that needs to create the same type of action, put the logic for creating that action in one place.
  2. Documentation: The parameters of the function act as a guide for what data is needed to go into the action.
  3. Brevity and DRY: There could be some larger logic that goes into preparing the action object, rather than just immediately returning it.
  4. Encapsulation and consistency: Consistently using action creators means that a component doesn't have to know any of the details of creating and dispatching the action, and whether it's a simple "return the action object" function or a complex thunk function with numerous async calls. It just calls this.props.someBoundActionCreator(arg1, arg2), and lets the action creator worry about how to handle things.
  5. Testability and flexibility: if a component only ever calls a function passed in as a prop rather than explicitly referencing dispatch, it becomes easy to write tests for the component that pass in a mock version of the function instead. It also enables reusing the component in another situation, or even with something other than Redux.

Dispatching Actions from Components 🔗︎

There's a number of semantically equivalent ways to dispatch Redux actions to the store from a component. For example, all these do the same thing:

// approach 1: define action object in the component
this.props.dispatch({
    type : "EDIT_ITEM_ATTRIBUTES", 
    payload : {
        item : {itemID, itemType},
        newAttributes : newValue,
    }
});

// approach 2: use an action creator function
const actionObject = editItemAttributes(itemID, itemType, newAttributes);
this.props.dispatch(actionObject);

// approach 3: directly pass result of action creator to dispatch
this.props.dispatch(editItemAttributes(itemID, itemType, newAttributes));

// approach 4: pre-bind action creator to automatically call dispatch
const boundEditItemAttributes = bindActionCreators(editItemAttributes, dispatch);
boundEditItemAttributes(itemID, itemType, newAttributes);

To me, the simplest and most idiomatic approach is to always use pre-bound action creators in your components. This removes the need for components to worry about dispatch, and makes them agnostic to Redux itself. In addition, I highly encourage the use of the object literal shorthand for binding actions with connect:

import {connect} from "react-redux";
import {action1, action2} from "myActions";


const MyComponent = (props) => (
    <div>
        <button onClick={props.action1}>Do first action</button>
        <button onClick={props.action2}>Do second action</button>
    </div>
)

// Passing an object full of actions will automatically run each action 
// through the bindActionCreators utility, and turn them into props

export default connect(null, {action1, action2})(MyComponent);

For a while, I used a utility function that could generate my mapDispatch functions, and ensure that all my bound action creators were available under a prop named actions (such as this.props.actions.someCallback()). However, I've changed my mind on that approach, and am phasing that out of my codebase as I go. I liked the explicitness of the intent, but eventually decided that it was too much of a conceptual coupling to Redux. My components had to "know" whether they were connected or not by knowing whether to call this.props.actions.someCallback() or this.props.someCallback(). It also introduced a bit of extra complexity and inconsistency.

Final thoughts 🔗︎

I know that many people dislike adding extra layers of indirection and abstraction, to which I sympathize. I've always been a very pragmatic, direct, "let's write the code that actually does this" kind of developer, and I hate HammerFactories and enterprise architectures where everything is configured with a 15,000-line XML file. I've also seen concerns that prolific use of Redux reduces React to nothing more than a "fancy templating language", or a reinvention of MVC under another name. But, to me, consistent use of action creators promotes readable code with a reasonable level of abstraction, is in keeping with software engineering principles of encapsulation and DRY, and provides just enough decoupling between "display logic" and "business logic" to keep a codebase manageable and flexible.

Further Information 🔗︎


This is a post in the Idiomatic Redux series. Other posts in this series: