blog.jakoblind.no

Redux Action creator best practice

When coding complex Redux applications, you can come to a situation where you have more than one potential solution to a given problem. But you don’t yet have the experience to know what are the pros/cons to each solution. That makes it difficult to pick the best one.

There are some best practices created from experience by the community and the Redux/Flux authors that you can use as tools when you get stuck.

How to think about Action creators

Have you ever thought about why Redux uses Actions and Reducers for state changes? Why are they separated?

Redux Actions and Reducers are not separated by coincidence. The original author Dan Abramov recently had an interesting Twitter thread where gives the answer.

The Actions handles “what happened” and the Reducers handles “how to change the state”.

Because of this design decision, the components calling Actions only needs to know one thing: “what happened”.

The calling component doesn’t need to know how to change the state. It doesn’t even need to know that the state is changed at all. Or that Redux is being used to change the state.

This has some other advantages:

  • Easy to change state management lib.

  • Easy to reuse components in other contexts where state changes should be handled in another way.

  • The “what happened” and “how to change the state” are decoupled. Decoupling is a good practice for clean code.

  • Increased testability. You have a separate test for state changing logic and the logic that happens before a state change.

It doesn’t come for free

An Action creator is just a regular JavaScript function. It means that you as a developer can use it any way you want. There is no limitation on what you can name it or what it should do.

This means it is possible to use it “wrongly”.

Consider the following component calling action creators:

class UserForm extends React.Component {
  // This function is called when the form is submitted
  formSubmitted() {
    // These are calls to action creators:
    this.props.setUserId(this.state.user.uid);
    this.props.setName(this.state.user.name);
    this.props.setEmail(this.state.user.email);
  }
  render() {
     // Code for rendering a form
  }
}

We can tell from the calls to the Action creators that userId, name, and email are set in the Redux state. That is a description of “how to change the state” - not a description of “what happened” in the component.

An implementation that shows “what happened” would look like this:

class UserForm extends React.Component {
  // This function is called when the form is submitted
  formSubmitted() {
    // This is an Action creator call:
    this.props.updateUser(this.state.user);
  }
  render() {
     // Code for rendering a form
  }
}

A user action is a Redux action

Redux is an implementation of Flux. The Flux documentation has an interesting description of how Actions should be used.

Actions should be semantic and descriptive of the action taking place. They should not describe implementation details of that action. Use "delete-user" rather than breaking it up into "delete-user-id", "clear-user-data", "refresh-credentials" (or however the process works)

In our day to day programming sessions we can use a simple rule of thumb:

A user action should be an Action creator.

Use case: Multiple simultaneous state changes

Let’s say you have a modal where you can edit the username in a text field. And there is a save button. When the user clicks the save button two things should happen simultaneously: It should update the Redux state with the newly inputted data, and it should close the modal by changing the UI state. We need to do two state changes simultaneously.

It can be tempting to create two Action creators in this case because two state changes should happen.

But the user does one action: closes the modal. That means that it belongs to one action creator.

So how do we update the state on two places from one action creator?

There is a common misunderstanding that there should be a one-to-one mapping between Action creators and reducers. But that is not true. The Action dispatched by the action creator can be picked up by many reducers. Let’s look at some code.

We create an Action creator that looks like this:

export function editUsername(userName) {
  return {
    userName,
    type: "EDIT_USERNAME"
  };
}

Next, we will extend two reducers we have in our app: uiReducer and data Reducer. They will both listen to the action CLOSE_MODAL.

function uiReducer(state, action) {
  switch (action.type) {
  // ...
  case 'EDIT_USERNAME':
    return { ...state, modalVisible: false };
  default:
    return state
  }
}
function userReducer(state, action) {
  switch (action.type) {
  // ...
  case 'EDIT_USERNAME':
    return { ...state, userName: action.userName };
  default:
    return state
  }
}

Every Action dispatched will go through all reducers. It will continue even on a “hit”. So in our example, both the uiReducer, and the userReducer will update it’s state when EDIT_USERNAME is sent.

Conclusion

Redux is a library that gives you freedom in how to write your code. This means that you need to be extra careful to think about how you structure your code. Otherwise, it can turn into a mess.

Whenever you are in a situation where you are unsure of how many Action creators you should make or what to name them, think of this rule of thumb: A user action should be an Action creator.


tags: