---
title: "Solving React Redux Triggering Too Many Re-Renders"
date: 2022-09-18T18:13:31-04:00
toc: false
images:
tags:
- javascript
- typescript
- react
- web
- dev
---
This might be obvious for some, but I was struggling with a performance issue in
[Bulgur Cloud](/portfolio/#bulgur-cloud), my React (well, React Native) based
web application. Bulgur Cloud is an app like Google Drive or NextCloud, and one
of the features is that you can upload files. But I noticed that the page would
slow down to a crawl and my computers fans would spin up during uploads. It
can't be that expensive to display a progress bar for an upload, so let's figure
out what's happening.
As the first step, I installed the "React Developer Tools" extension in my
browser and enabled the "Highlight updates when components render" option. Then
I used the network tab to add a throttle so I could see the upload happen more
slowly, and started another upload.
{{}}
{{}}
Pretty much the entire screen flashes every time the progress bar goes up.
Something **is** causing unnecessary re-renders! The next step in my diagnosis
was to record a profile of the upload process with the react developer tools. I
enable the option "Record why each component rendered when profiling", then run
the profiler as I upload another file.

Walking through the commits in the flame graph (the part that says "select next
commit" in the screenshot, top right), I can see this weird jagged pattern that
repeats through the upload process. Selecting on of the tall commits again
confirms that pretty much everything had to be re-rendered. Hovering over the
items being re-rendered shows me that the items being re-rendered are my folder
listings, the rows representing files. It also tells me why they re-rendered:
"Hook 3 changed".
Next step is to switch over to the components feature of the react developer
tools, and take a look at `FolderList`. Once I select it, I get a list of the
hooks that it uses. Keep expanding the tree of hooks, and the hook numbers are
revealed.

The hook names here seem to be the names in the source code minus the `use`
part, so my `useFetch` hook becomes `Fetch`. They are in a tree view since hooks
can call other hooks inside them. Following the tree, hook 3 is `State`, and is
located under `Selector` which is a React Redux hook `useSelector`. At this
point things become clearer: I'm using redux to store the upload progress, and
every time I update the progress it causes everything to re-render. And all of
this is being caused through my fetch hook. Let's look at the code for that:
```ts
export function useFetch(
params: RequestParams,
swrConfig?: SWRConfiguration,
) {
// useAppSelector is useSelector from react redux,
// just a wrapper to use my app types
const { access_token, site } = useAppSelector((selector) =>
// pick is same as Lodash's _.pick
pick(selector.auth, "access_token", "site"),
);
// ...
```
I realized the issue once I had tracked it down to here! The state selector is
using the `pick` function to extract values out of an object. React Redux checks
if the value selected by the selector has changed to decide if things need to be
re-rendered, but it uses a basic equality comparison and not a deep equality
check. Because `pick` keeps creating new objects, the objects are never equal to
each other, and Redux keeps thinking that it has to re-render everything!
The solution luckily is easy, we can tell redux to use a custom function for
comparison. I used a `shallowEquals` function to do a single-depth comparison of
the objects (the object is flat so I don't need recursion).
```ts
export function shallowEquals, Right extends Record>(left: Left, right: Right) {
if (Object.keys(left).length !== Object.keys(right).length) return false;
for (const key of Object.keys(left)) {
if (left[key] !== right[key]) return false;
}
return true;
}
// ...
const { access_token, site } = useAppSelector((selector) =>
pick(selector.auth, "access_token", "site"),
shallowEquals
);
```
Let's look at the profile now:

Much better! The only thing re-rendering now is the progress bar itself, which
is ideal.