•about 5 years ago
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:
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!
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.
- Either you store them inside an object where each key is the activity id and the value the activity.
- 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:
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:
To avoid this you need to store the event’s property you are interested in inside its own binding:
- 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:
We use the Ducks methodology to organize our code like this:
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:
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:
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).
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: