(Korean, Japanese)

Redux is a surprisingly simple library for managing application state with a Flux-like architecture. Here at Affirm, we are particularly interested in Redux’s time-travel capabilities. Our core business is offering transparent consumer loans, so it’s incredibly valuable to be able to replay the entire loan application process from the user’s perspective.

Redux is more of a set of functions that help enforce a pattern rather than a framework. That said, if you aren’t careful about enforcing good patterns, you might find yourself regretting your decision to use Redux. In this article, we’ll go over some Redux best practices we’ve established at Affirm, as well as some common pitfalls we’ve encountered.

ImmutableJS

ImmutableJS is a library for immutable persistent data structures. There are two reasons we might want to use this library.

The first is that there are several benefits that come with using immutable data. There are plenty of articles that talk about this, but the gist of it comes down to referential transparency. When you’re allowed to mutate an object in-place (i.e. without changing the reference pointer), it’s hard to reason about your program.

At a higher level, mutable data makes it really hard to use mathematics as a form of analysis for your program — imagine a math theorem where x isn’t always equal to x. Practically speaking, with immutable data we can often use a cheap referential comparison (===) to see if an object has changed rather than a deep comparison. One example where this is very helpful is in determining if we need to re-render a React component.

The second reason to use ImmutableJS is performance. Working with immutable data can lead to garbage thrashing if you’re constantly cloning the underlying data structure any time you perform a mutation. To address this problem, ImmutableJS uses persistent data structures to efficiently make immutable updates, returning a new reference without cloning the underlying data.

Think about how a linked list works: To create a new linked list that has an item appended to the head, you can simply create a new node that points to the old head and return a reference to that new node. The previous list is persisted and the underlying data is shared with the new list. And so long as we ensure that there are no in-place mutations, we have an efficient means of working with immutable data.

However, ImmutableJS is only useful if you use it properly. There are two common mistakes I’ve seen. The first one is how to apply multiple mutations. Suppose you want to set multiple values in a Map. For example, below we are setting loading to false and updating the user property of the state.

state.set('loading', false).set('user', user)

When you set a value on a Map, ImmutableJS will do all kinds of rearranging under the hood (using a Hash Mapped Array Trie) so that the intermediate state is persisted. This is unnecessary work because we only care about the end result and not about any intermediate steps. So instead we can use the withMutations function to batch these updates and only do the rearranging once.

state.withMutations(s => s.set('loading', false).set('user', user))

Another anti-pattern is the regular conversion to raw JavaScript objects using .toJS() any time you want to work with the data. Here’s a classic example I’ve seen here at Affirm.

const mapStateToProps = (state) => ({

loans: state.get('loans').toJS(),

})

When we do this, we totally lose the performance benefits of using ImmutableJS because we’re effectively cloning an object every time we do this. Instead, we should leave the data as an ImmutableJS object and use the ImmutableJS methods like .get and .getIn instead.

const mapStateToProps = (state) => ({

loans: state.get('loans'),

})

Redux Actions

Here’s an example of a Redux action we had that makes a payment on a loan.

This code works fine, and if you’re new to Redux it’s probably what all of your actions look like. That said, there are several anti-patterns in the code above; we’ll flush out each one incrementally.

The first problem is the way we’re using errors for program logic. Using try-catch for program logic can easily mask actual errors in your code and prevent them from raising in the console. For example, suppose you spelled if (reponse.status) wrong? This will raise an exception that will get caught, making it very hard to trace bugs. So so let’s get rid of that catch statement.

Next, we should decouple the intention of the action from the implementation of the action. There’s no reason this action should concern itself with how the HTTP request is configured and sent. So let’s pull that out into its own file.

Now this action reflects its intention rather than its implementation. While we’re refactoring, let’s take care of that Pyramid of Doom by using the ES7 async-await syntax (note: you’ll need to use the Babel stage-0 preset along with the babel-polyfill).

Things are looking better, but we’re not done yet.

Another issue is that we’re over-dispatching causing unnecessary re-renders. Most people don’t realize that every time we dispatch an action, we feed it into the reducer, and then render the new state. In the example above, when the request comes back, we’re going to cause two renders immediately caused by closeModal and then either makePaymentSuccess or makePaymentFailed.

Dispatching the closeModal action also muddies up the intention of this action. makePayment should not be concerned with closing a modal window. Instead, we should perform this logic in the reducer upon receiving the success or failed actions.

We just killed two birds with one stone! The last thing we should do is refactor our synchronous and asynchronous actions into separate files. This makes the async actions much more testable since we can now spyOn the sync actions and assert when they have been called.

This test is effectively the inverse of the function itself. In fact, this calls into question whether we even need to test this function! I think that’s a sign of good coding practices.

Redux Reducer

Now that we’ve cleaned up our actions, let’s take a look at the reducer function we just defined above.

We’re properly using withMutations which is a good start, but we’ve failed to decouple the logic from the implementation causing us to repeat code in several places. What we can do here is separate each mutation into its own function and compose them together. Check this out.

Here we’re using a pipe function to take care of calling withMutations and applying each mutation function. This makes our reducer much more readable. And testing this reducer is going to be easy as well. It will effectively be the inverse of the function itself, just like with actions.

So again, we can ask ourselves if it’s even worth testing this function. What we really want to ensure is that those mutators are mutating the state in a way the UI expects. So we can write some unit tests for those, but those aren’t really useful if the UI is expecting the state in some other format. So perhaps we might get more bang for our buck if we use a static type checker like FlowType or TypeScript. But we’ll leave that for another day.

Conclusion

Redux can lead to some very clean, performant, and understandable code, but if you fall victim to using it the wrong way, it can do more harm than good. If you’re interested in learning more about these sorts of software patterns, I’d recommend checking out this talk — it really solidified in my mind some high-level concepts about how to write good code.

And if you’re looking for a job and are interested in working with React and Redux, check out our careers page or send me an email.