React, Redux, Unnecessary Rendering, and Computed Values

This is a continuation of my post where I discussed one of my React apps, “Drop”. Here I want to talk further about some of the internal logic and how it relates to switching over to Redux.

To understand the topic of this post, you need to understand a tiny bit about how the app works. Assume you have a number of data entries already. Each one is just a text string that may contain any number of #hashtags—that is, any word may be tagged with a hashtag, making that a keyword for the text, like “#apple”.

When you type text into the main text field, it parses the text to get a list of these hashtags. Thus, “Drop app, please search for #john and #birthday” gets parsed into the list [“#john”, “#birthday”]. The update happens with every keystroke. That could be debounced if necessary, but so far it doesn’t seem like a problem on modern computers.

In the background, the app is also performing a search. From all the available data entries, it searches for the ones that contain all of the hashtags in the current hashtag list. We call those the selectedEntries. (Actually, internally I call them “selectedDrops” because I call these text entries “drops”.) The app displays the list of selected entries to the user.

If you are worried about performance, then, a question presents itself. When does the list of selected entries get re-rendered on the front-end display? Does it re-render at every keystroke? Does it re-render only when the list of hashtags has changed? Does it re-render only when the list of selected entries has changed?

To my chagrin, when I examined my app I discovered that it was re-rendering the list on every keystroke. Specifically what I mean is this: I added a console log to the dumb view component that displays the list so it printed a message every time the render function was called. And it was called at every keystroke. That seemed less than ideal.

I might have known that’s how it would work. Internally, I was using a JavaScript getter function to generate the list of selected entries. Every time that value was accessed, it executed the getter function, it was actually creating a brand new Array object containing the selected entries. That alone is bad enough, even if it didn’t cause a re-render with the same data. I was well aware that using a getter function would cause the value to be re-computed every single time. It’s something I always intended to improve later. But I didn’t know it would cause a re-render every time like that.

I’ve done several tests on this subject using plain React, and I don’t know why but it re-renders things all the time even when the underlying data has not changed. Even when the value is a reference to the same object. To actually get some control over this, I decided to use Redux.

I began incrementally moving things over to Redux. I added Redux and react-redux. I moved some of my state variables into a Redux store and wrote the appropriate reducer function. Specifically, I have a state variable for the main text input and for the list of all data entires. I moved those into Redux. The Redux store and Redux actions will handle updates of the main text input. The list of data entries only changes when the user changes it by adding or deleting something.

The interesting thing here is the list of hashtags and the list of selected entries that are computed from those state variables. These values are not state, but they follow from state and I have to do those computations in order to render my views. Ideally, however, I don’t want to do a lot of unnecessary re-rendering, so I want to intelligently control when and how those computed values are re-computed.

My decisions about how to do this will not necessarily fit your situation, but the point is that you can use a Redux reducer function to mimic something like a computed value, and you can use this to gain more granular control over the app.

In my case, I decided to pretend that ‘hashtags” and “selectedEntries” were state variables. They have to be explicitly updated as appropriate inside my reducer function. This is what allows me to make those decisions myself instead of having someone else do it for me.

I made some changes as follows. Any time the text input changes, we do have to recompute the hashtags from the new text. However, I do not automatically update my state with this new array of hashtags. If I check them one-by-one and the arrays are actually the same, then I leave the current array in place. It’s a subtle distinction, but it does matter. I decided I don’t want to replace an array with a copy of itself. This means that when comparing old state to new state for other purposes, you can check if the value of the hashtags array has changed by using the equality comparator, “===”. If they are the same, then they will actually be the same object in memory.

Is this actually more efficient than what React would do otherwise? Frankly, it’s hard to say. I think it probably has to do with how complex the rendering function itself is. One for-sure advantage of this is that I know how it works. It’s not just magic.

With that in place, I also change how the selectedEntries array is computed. If the hashtags array and the allEntires arrays have not changed, then selectedEntries does not change. That is: if the value of hashtags and the value of allEntires in the new state both point to the same array object as in the previous state, then selectedEntries is also the same object in memory. It will only be computed if one of those arrays actually changes.

If you use react and react-redux in your app and correctly pipe your components through the “connect” function and all that, then this works and the components do not re-render unnecessarily when their props remain unchanged. Like I said, plain React does re-render and I have no idea why.

I will say I’m quite happy with this change. I don’t see any unnecessary re-rendering in my app now—at least, not in the local version I’m working on right now. I haven’t published all these changes yet.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s