--- 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. ![A flame graph with many elements highlighted in blue.](/img/react-redux-rerender-flamegraph.png) 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. ![A tree view displaying hooks for a component named FolderList](/img/react-redux-component-hooks.png) 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 my object! 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: ![A flame graph with only a tiny portion of elements highlighted.](/img/react-redux-after-flamegraph.png) Much better! The only thing re-rendering now is the progress bar itself, which is ideal.