React/Redux – Best Practices & Gotchas

Nick P.
Nick P.
Published July 12, 2016 Updated March 25, 2021

This is a bonus post in the Cabin tutorial series created by getstream.io. Visit getstream.io/cabin for an overview of all the tutorials, as well as a live demo. The source code can be found on the Stream GitHub repository for Cabin, and all blog posts can be found at their respective links below:

  1. Introduction
  2. React
  3. Redux
  4. Stream
  5. Imgix
  6. Keen
  7. Algolia
  8. Mapbox

At Stream, we’ve chosen to use React and Redux to power both our own dashboard and our example application, Cabin. This decision wasn’t based (solely) on the fact that React is the new hotness in the Javascript world. Stream stands by this combo because the two are simply good at displaying data and managing state, and we're not the only ones who feel this way. React and Redux power scalable, production-level applications used by millions of people every single day.

Goals for this article

By the end of this article you should be able to:

  • Clearly articulate best practices when it comes to React and Redux.
  • Avoid common React/Redux pitfalls.
  • Structure a production level application that can scale.
  • Utilize tools such as Immutable.js, Chrome’s React developer tools, and middleware, such as Raven.
  • Understand the React event system.

This post is not: Please take a moment to read Thinking in React, as the article helps one see the advantages of pairing React and Redux.

  • A tutorial – there will be little code displayed in this post. If you’re interested in something more in depth, take a look at Cabin, a series of tutorials that results in a fully functional application.
  • A deep discussion on MVC frameworks, such as Angular. We could go on for days about which framework you should go with; however, we're not talking about this subject.

With all of that said, let’s get started!

Gotchas

A Case for Using Immutable Data Structures with Your React Application A quick refresh on what immutable data structures are: Mutative API’s yield a new instance instead of changing the old object. This model aligns well with the architecture of React and Redux since data is passed from above rather than being subscribed to. If you have read about the advantages of using immutable data structures inside your React and Redux application, but are not fully convinced you should use them (standard Javascript objects seem to work equally well right?), let me explain why you should always start a new React and Redux project with immutable data structures. As you have learned while reading the Redux documentation, a reducer is the only place that alters the application’s state. It is a pure function that receives the old state and an action as inputs and returns the next state. If you try to use a reducer to mutate input, Redux will warn you that this is not a reducer’s intended function. Mistakingly mutating the old state here is rare and using an immutable object to represent the state not necessary. But what happens if we use mutable objects to contain the state tree and mutate these outside of a reducer? Say we want to enrich some part of the state tree while mapping the state to properties of a certain React component (e.g. inside the “react-redux” connect method).

js
function mapStateToProps(state, ownProps) {
  let activities = state.feeds[ownProps.feedID];
  return {
    activities: enrichActivities(activities, state),
  };
}
function enrichtActivities(activities, state) {
  for (let activity of activities) {
    // Read user from state by user id
    activity.user = state.users[activity.userID];
  }
  return activities;
}

React does not warn us of this “illegal” mutation, neither does Redux. The application does not crash immediately, but we have now altered the application-wide state object. This means the state has changed by a side-effect other than a reducer function. This scenario can result in major headaches for developers when parts of the application seem to be breaking without any apparent reason. You can see that the state of the application is wrong with the help of Redux Dev Tools, but can not find out why and or where it was mutated. Using a library like Immutable.js mitigates this issue because it makes it impossible to mutate the state tree in any part of your code. Every operation performed on the state object will yield a copied instance, instead of changing the current one. Designing the Redux.js State Tree Designing the state tree is one of the first actions you will take when starting up a React/Redux application. It is possibly a good idea to consider several tradeoffs implicit in various design decisions before you start coding up your app. Here I will discuss two such tradeoffs. Often, you want to store a list of items retrieved from your backend inside the Redux state tree. At Stream, for instance, we would need to store activities retrieved for a feed. You have two options for storing these.

  1. Either you store them inside an object where each key is the activity id and the value the activity.
  2. Or you store them inside a list where each item is an activity.

The first approach has an advantage that it is easy to get activities by id from the state tree, as you only need to select a member by activity id. On the downside, you can not guarantee any order. Storing the activities inside a list allows you to guarantee ordering but selecting one item from the list by activity id means traversing the list. Denormalized or normalized state? Once your app starts growing more complex entities in the state tree will start referencing other entities. For example, in todo applications, todo lists may contain multiple todos. Your API might return one todo list nested with multiple todos, and thus it might seem like a good idea to store the todos that way in the state tree:

js
    {
      "title": "Todo List",
      "todos": [
        {
          "text": "Consider using Stream",
          "completed": true,
        },
        {
          "text": "Finish reading the Cabin blogpost series",
          "completed": false
        }
      ]
    }

This design decision makes it much harder for future actions/reducers to alter any of these todo items. They would have to be aware of the parent todo list before they could alter any todo inside it. Also moving a todo to a different list is more complex. Instead, try to normalize all relationships inside the state tree, either by keeping a list of references from a parent entity (i.e. the todo list) or keeping a “foreign key” in reference to the parent entity from a child (i.e. a todo item). These two approaches have different advantages, in the first approach, it is easier to order children, whereas moving an entity to a different parent is easier with the second approach.

Pro Tip: If your API returns deeply nested items and you need to normalize them on the client, the normalizr library might come in handy. React's Synthetic Events Inside React event handlers, the event object is wrapped in a SyntheticEvent object. These objects are pooled, which means that the objects received at an event handler will be reused for other events to increase performance. This also means that accessing the event object’s properties asynchronously will be impossible since the event’s properties have been reset due to reuse. The following piece of code will log null because the event has been reused inside the SyntheticEvent pool:

js
    handleClick(event) {
      setTimeout(function() {
        console.log(event.target.name);
      }, 1000);
    }

To avoid this you need to store the event’s property you are interested in inside its own binding:

js
    handleClick(event) {
      let name = event.target.name;
      setTimeout(function() {
        console.log(name);
      }, 1000);
    }

Best Practices

Debugging with Chrome's React Developer Tools Debugging Javascript can be cumbersome, especially if it’s a React app. React Developer Tools is a system that allows you to inspect a React Renderer, including the Component hierarchy, props, state, and much more. We find this to be a must-have in your toolbox. How To:

  • Arrow keys or hjkl for navigation
  • Right-click a component to show in elements pane, scroll into view, show source, etc.
  • Use the search bar to find components by name
  • A red collapser means the component has state/context

Pro Tip: If you inspect a DOM node in the Chrome Dev Tools and switch over to the React Dev Tools, it will expand the React component tree to identify the component that is responsible for rendering this DOM node. How to Structure Your Application If you’re like me, figuring out the proper structure for your application can be a task in and of itself. I find myself constantly thinking about scalability, cleanliness of code, and, most importantly, how other developers on my team will navigate through the codebase. Luckily for us, we stumbled across Ducks, a proposal for bundling reducers, action types and actions when using Redux in your application. Ducks provides developers with a unique and very nice design pattern. Instead of doing something like this:

bash
    |_ containers
    |_ constants
    |_ reducers
    |_ actions

We use the Ducks methodology to organize our code like this:

bash
    |_ containers
    |_ modules

Each module contains all of its related constants, actions/action creators, and it’s reducer. If any of our other modules need access to any of these elements (which will likely be the case in the case of a messaging module), we export/import what is needed. Logging Errors with Raven At Stream, we utilize Sentry to manage crash reports for our backend and frontend applications. To make a React crash report inside Sentry extra valuable, we append the current state of the application to any error that occurred by applying this middleware to the Redux store:

js
function createRaven(dsn, cfg = {}) {
  /*
        Function that generates a crash reporter for Sentry.
        dsn - private Sentry DSN.
        cfg - object to configure Raven.
      */
  if (!Raven.isSetup()) {
    console.error("[redux-raven-middleware] Sentry Raven is not configured.");
  }
  return (store) => (next) => (action) => {
    try {
      return next(action);
    } catch (err) {
      console.error("[redux-raven-middleware] Reporting error to Sentry:", err);
      // Send the report.

Pro Tip: In our example, we use Raven (the Sentry error logger) to capture errors, but this snippet can be reused with any crash reporting service such as Twitter’s Fabric. Building Production Level Apps with Webpack On the React website, you can find two different builds of the React library: a development build and a production build. The difference between these two builds is that the development version includes extra warnings about common mistakes, whereas the production version includes extra performance optimizations and strips all error messages. At Stream, we leverage the power of webpack, a module bundler, to make sure the output of the module bundler is the same as React’s production build, we have to inject a global variable named: process.env.NODE_ENV. With webpack, this can be achieved with the DefinePlugin. Add the following plugin to your webpack production build:

js
    new webpack.DefinePlugin({  'process.env.NODE_ENV', JSON.stringify('production') })

Now your output bundle will behave the same as the prebuilt production distributable found on the ReactJS website. Pro Tip: If you upgrade to React 15.2.0, you will get useful error messages from production React, as well (with links to a page explaining the error that occurred).

Conclusion

With the best practices and gotchas outlined above, you now have the ability to work through tough scenarios in a fraction of the time. The Cabin tutorial series, including this bonus post, is created by getstream.io. Try the 5-minute interactive tutorial to learn how Stream’s API work. If you're hungry for more, I’ve listed a few of our favorite tools, talks, and plugins below: