Javier Casas

A random walk through computer science

3 Common Intermediate React/Redux mistakes

Once you know a bit of Redux, you have learned to use new tools for managing global state. That doesn't mean that you know how to do Redux right, so let's check three types of mistakes you are likely to make.

Not creating functions in connect to dispatch actions

When using connect, it's too easy to pass the dispatch function to the component so that the component can dispatch whatever action it pleases. As a result, you end up with:

  • Components that have to know that they run under Redux, and that they have to dispatch actions for the specific store they work with.
  • Components that have no clear interface. All they do is receive a dispatch, and who knows what will they do with that dispatch?

This couples your components to your Redux implementation, causing your components to have to do too much and bear too many responsibilities. Instead of this, focus on putting the code that joins Redux and your components only in the connect call. This will make your components clean and easy to understand, and these components will receive nice and descriptive functions such as onLoginSubmitted or onClientFormSubmitted. Running these functions will trigger operations in Redux or whatever, but it's not the responsibility of the forms to understand what will happen when they trigger those operations. Their responsibility is triggering these operations at the right time, and nothing more.

Thinking of Redux as nothing more than a glorified getter/setter

One of the things that many tutorials tells you is to create actions for setting this value and that value. Setting values in a global object is something that you have to do many times. But there is way more that Redux can do for you than just setting stuff. Even further, thinking that all that Redux does is set stuff in global variables will lead your designs to have too many simple actions that do too little, and have to be called in the right order for the whole thing to not fall apart.

You can see this bad pattern in practice if you find that the majority of your actions are called SET_*, and all they do is call a reducer that overwrites a field with a fixed value from the action. Then, the next component expects that all the fields in the store have been previously filled, and fails miserably if a field wasn't filled properly. Which means you have to call a bunch of SET_* actions in a somewhat-defined order for the next component to not explode in flames. Even worse, if you forget to call SET_* on the second run of this workflow, everything works, except some of the data was from the previous run, and now we are assigning ClientY the password of ClientX.

You have to think of your store as a state machine, and you have to think of your actions as operations that will trigger changes in that state machine. What is the initial state? What transitions do make sense? What are the prerequisites to reaching some state? How do we ensure those prerequisites get confirmed before trying to reach that state?

function createUserReducer(state=initialState, action) {
  switch(action.type) {
    case "NEW_USER":
      // Reset store
      return initialState

    // Dispatched by the user/password form
    case "SET_LOGIN_CREDENTIALS":
      return {
        ...state,
        username: action.username,
        password: action.password,
        currentForm:
          // If we already had an avatar, we can proceed to confirm create the user
          // because we have all the fields required
          // otherwise, we have to go to upload the avatar
          state.avatarUrl === undefined
            ? "DISPLAY_AVATAR_FORM"
            : "DISPLAY_CONFIRM_FORM",
      }

    // Dispatched by the avatar upload form
    case "SET_AVATAR_URL":
      return {
        ...state,
        avatarUrl: action.avatarUrl,
        currentForm:
          // If we already had username and password, we can proceed to confirm create the user
          // because we have all the fields required
          // otherwise, we have to go to the user/password form
          (state.username === undefined || state.password === undefined)
            ? "DISPLAY_USER_PASSWORD_FORM"
            : "DISPLAY_CONFIRMATION_FORM",
      }
  }
}

On the example above, it can be controversial where to put the logic that goes either to the user/password form, to the avatar form or to the confirmation form. You can put it on the reducer, or you can put it on a component switch. My point is that you have to think of the states of your application, in order to be able to think where you want to put that logic. Without that logic in place, it will be easy to bypass steps in your application, and the correctness of the whole process will be degraded. Without thinking on the underlying state machine, your application will have very slim chances of being correct.

Dispatching multiple actions for a single event

This always starts in an innocent way. You have some kind of operation that calls the API and returns with a single value, which you dispatch to your store to trigger updates and all that stuff.

function fetchSingleClient(...) {
  dispatch({type: "START_FETCHING"})
  return fetch(...)
    .then(client => dispatch({type: "CLIENT_FETCHED", client}))
}

Everything is fine, and then, some day, you have to deal with multiple calls to the API. So you ask the backend team for a batch operation API endpoint. And now you call this endpoint, which returns many values, and you dispatch an action for every value that the API returned.

function fetchManyClients(...) {
  dispatch({type: "START_FETCHING"})
  return fetch(...)
    .then(clients =>
      // This forEach is going to be painful
      clients.forEach(client => dispatch({type: "CLIENT_FETCHED", client}))
    )
}

And now your frontend is slow as it runs reducers and an update cycle for each of the many values returned by the API.

It is too easy to reuse actions when we go to multiple values, because you already have the actions for a single value. It is too easy to dispatch many actions, all you need is a for loop. it is too easy to not catch it in development, because you try it with a batch of three values, and it's fast enough with such small batch on your fast development machine. But it all goes wrong when the batch in production is of 100 values, and it has to run in an old slow mobile phone.

Each React/Redux re-render cycle can be expensive, because we have to run all the mapStateToProps functions in the app, and compare previous and new values, and then maybe even re-render stuff. You don't want to do that very often. You really don't want to do it 100 times every call to the backend succeeds. Which means:

  • You have to create batch actions and corresponding reducers.
  • You have to dispatch a single batch action for each event (such as completing an API call).

So that when the batch call succeeds we run a single expensive re-render cycle, instead of many.

function fetchManyClients(...) {
  dispatch({type: "START_FETCHING"})
  return fetch(...)
    .then(clients => dispatch({type: "CLIENTS_FETCHED", clients}))
}

The good thing is that, once you have batch actions, you can remove your previous single-value actions, because they should be equivalent to your batch actions with a batch size of 1.

function fetchSingleClient(...) {
  dispatch({type: "START_FETCHING"})
  return fetch(...)
    .then(client => dispatch({type: "CLIENTS_FETCHED", clients: [client]}))
}
Back to index
Copyright © 2018-2023 Javier Casas - All rights reserved.