Skip to content

React Hooks and Tips to Avoid Useless Component Render Applied on Lists

January 31, 2022Louise Loisel13 min read

List and React logo

A few weeks ago, I encountered children list rerender issues on the project I was working on. In this article you will learn :

  • how I debugged a react performance issue
  • why virtualization is not always suitable for list rendering issues
  • what is memoization
  • how to memoize react components and functions with react hooks and React.memo() to prevent a component from re-rendering.

Overview on the performance issue

I’m working on a platform to purchase orders from suppliers. Each order is composed of clothing items. Here is a typical example of the interface.

website screen

Let’s take a very simple example: I am ordering from my supplier Theodo, producing some top quality shirts. I want to order their famous shirts in three colors (white, red and blue). On the left there is a sidebar where you can select a card, here there are three cards, one for each color. On the right there is a form with the information relative to the selected card.

When another card is clicked, the form values on the right is now displaying the information related to the new selected item.

For the new year, a big order has to be done containing more than 600 items. In this situation, the list takes a huge amount of time to load, or doesn’t even load at all. And if I’m lucky enough to have the 600 items displayed inside the sidebar, when another item is clicked on, I’m also waiting... This performance issue is terrible in terms of user experience and needs to be fixed!

website.gif

First, I need to make sure whether the performance issue actually comes from React. To do so, the user interaction can be recorded with the Google devTools performance tab. When JavaScript is executed it is accounted in scripting. Here, scripting is taking most of the time. I can investigate more on what is happening.

Script phase taking 80 percents of total time


I then used the React profiler to see if this long scripting task comes from long react render times or useless component re-renders. The profiler is included in the React Dev Tools Chrome extension. It records renders, why your components rendered and for how long.

Long story short, you mainly need to understand that a colored bar means that your component rendered, grey bar means it did not.

profiler when component renders It did render
profiler when component does not renders It did not render


And guess what? When an item in the list is clicked on, all the items contained in the list re-rendered 🥵 ! When the list is only 3 components long, it is ok to recompute but with 600 components, each click becomes quite expensive.

For example here is the result in the profiler when the second card in the sidebar is clicked on. Basically, everything re-rendered.

initial profiler Profiler view when an other card is clicked on, everything renders

So the React rendering step needs to be lightened. What options do we got to perform this?

  1. Find a way to reduce the number of items that need to re-render

    It can be done thanks to virtualization. Virtualization is a technique to compute in the DOM only the elements located inside the user's viewport. When the user scrolls, elements will be added/deleted in the DOM. A few librairies already implement this for us https://github.com/bvaughn/react-window.

  2. Find a way to avoid useless re-renders

Why virtualization is not the right solution here?

Virtualization is a good option when your rendering phase is taking a long time. But here, it’s the re-renders that take time. If re-renders issues are solved, displaying 600 items only once is not an issue.

Moreover, the average user is making orders of about 50 items without a powerful computer. Virtualization is not very useful for short lists.

Finally, the height of each item card is variable (when a field is filled, the item card will grow), and the number of item is variable (the list can be filtered, items can be created or deletes). Windowing librairies are not well suited for variable items size or number, some custom functions have to be added to make it work. For the user this can lead to a laggy scroll. And re-renders issues won’t be solved.

So let's solve those useless re-renders!

Why do components re-render when another card is clicked on?

The structure is the following: OrderItemsSelection is the parent component, it contains the Form section and the Sidebar. The Sidebar itself has children: the SidebarCards (as many as items).

components full tree
OrderItemsSelection has a state, selectedItem, and a state setter, setSelectedItem for the selected item id (thanks to a useState hook).

OrderItemsSelection passes selectedItem as a prop for the Form section and the Sidebar.

OrderItemsSelection also passes setSelectedItem setter to the Sidebar, Sidebar passes the setSelectedItem setter to its SidebarCard children to be used on card click.

So when the red shirt card is clicked on, a new state is setted with setSelectedItem, the selected card is now the red shirt card.

As the selected card is a state of the component OrderItemsSelection, it re-renders: a component state change triggers a re-render. Therefore all its children also re-render: the Form section, and the Sidebar. As the Sidebar re-renders, so are its children: the white/red/blue shirts cards.

components hooks In "components" tab the value attributed to Hook 1 is displayed (which caused a render)
app initial state updating schema Component updating schema


But in fact what do we truly need to render in the sidebar? If we take a closer look, the blue shirt card does not need to be rendered as none of its props actually change.

How to prevent re-renders

What is memoization?

Let’s take a look at Wikipedia definition:

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

So here, the Sidebar is a parent component of each card component. So we want to cache the blue card component computations because it has unchanged prop values, ie memoize this component. We can find more information about component memoization in the React documentation:

React documentation:

React.memo is a higher order component. If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

Memoize a React component with React.memo()

Ok so if everything goes as planned I just have to add the memoization to SidebarCard component, and React will magically understand that if props values didn’t change, the component does not need to re-render.

      export default SidebarCard;
Before memoized component
      export default memo(SidebarCard);
After memoized component


But it would be too easy won’t it be?

expected not rendered third component schema Expectations
rendered third component schema Reality


When we launch the profiler and focus on the blue shirt card:

Profiler with memoized component

We can see that components are memoized thanks to (Memo) just after component name, but we can also see that React considered that props did change for the last card: sidebarItems and onSetSelected. What’s interesting is that it recognized that the isSelected boolean did not change. It’s a boolean so the comparison is only made based on the boolean value. With functions, array and object, it’s different. Even if the objects have the same properties, they are considered equal only if it refers to the exact same object.

const Laura1 = { name: "Laura" };
const Laura2 = { name: "Laura" };

Laura1 === Laura2;
// => false

Functions, as well as arrays and objects are stored by reference into memory and not by value. We call these non-primitive data types as opposed to primitive types (string, boolean, number, undefined or null) which are stored by value. Here is a great article to understand all about it.

Memoize a function with useCallback()

This means that a new function onSetSelected was created earlier, which has the same value as the former but not the same reference. They are two different entities even they look alike.

onSetSelected is a function that is called when a card is clicked on to set the new selected item id.

onSetSelected is passed from the sidebar to the card here:

const Sidebar = ({
  onSetSelected, // => received here
  sidebarItems,
  selectedItemId,
  allowMultiSelect = false,
}: SidebarProps): JSX.Element => {
return (
  <div>
    {sidebarItems.map((sidebarItem, index) => (
      <SidebarCard
        sidebarItem={sidebarItem}
        positionIndex={index}
        onSetSelected={onSetSelected} // => passed here
        isSelected={selectedItemsId === sidebarItem.id}
	    />
     ))}
  </div>


onSetSelected is defined in the Sidebar parent:

const OrderItemsSelection: React.FunctionComponent = () => {
  const {
    initialValues,
    submit,
    selectedItemId,
    setSelectedItemId,
  } = useItemsSelection();

  // => onSetSelected definition
  const onSetSelected = (index: number) => {
    setSelectedItemId(index);
  };

  return (
    <Formik initialValues={initialValues} onSubmit={submit}>
      {({ values, validateForm }: FormikProps<ItemsSelectionValues>) => {
        return (
          <>
            <Sidebar
              sidebarItems={values}
              onSetSelected={onSetSelected} // => passed here
              selectedItemId={selectedItemId}
            />
            <ItemsSelectionForm
              initialValues={values.items[selectedItemId]}
              selectedItemId={selectedItemId}
            />
          </>
        );
      }}
    </Formik>
  );
};

So when the OrderItemsSelection component re-renders, a new onSetSelected is generated. The blue shirt card receives a new function (weirdly same looking as the previous one). So it has to re-render.

We want to avoid recomputing the function, as nothing has changed except the re-render in OrderItemsSelection. In short, we need to memoize the function. Fortunately, React has already implemented this for us. A big welcome to the beautiful useCallback hook which will memoize our function!

const onSetSelected = (index: number) => {
      setSelectedItemId(index);
      };
Before without useCallback
    const onSetSelected = 
      useCallback((index: number) => 
      {
        setSelectedItemId(index);
      }
    , []);
With useCallback


Now it’s time to try if our onSetSelected is still a guilty prop.

Profiler only item props changed Profiler with memoized onSetSelected function
Schema only item props changed Component updating schema

Nice shot, onSetSelected is not guilty anymore, but we still have one targeted prop: the sidebarItem .

Use React.memo() comparison function to tell React when a component should re-render

Same protocol, as done before, where is sidebarItem defined?

const Sidebar = ({
  onSetSelected,
  sidebarItems,
  selectedItemId,
  allowMultiSelect = false,
}: SidebarProps): JSX.Element => {
return (
  <div>
    {sidebarItems.map((sidebarItem, index) => ( // => defined here
      <SidebarCard
        sidebarItem={sidebarItem} // => passed here
        positionIndex={index}
        onSetSelected={onSetSelected}
        isSelected={selectedItemId === sidebarItem.id}
      />
    ))}
  </div>

When Sidebar renders it recomputes the map, and each sidebarItem is recomputed. Since it’s a brand new object and as explained earlier, with a new reference, so it is not equal to the previous object even if it contains the exact same values. So how can we tell our card component to take into account only deep equality of previous and current sidebarItem? Once again, React team has already a solution right in the React.memo doc:

By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.

Nice, the next step is to define a function that can compare the SidebarCard props in a customized way.

Stringify is not suitable for deep comparison

We could have thought to compare the stringifying version of the two objects, but it’s not the best idea as when we have equal properties not in the same order, it would have returned false.


    const Laura1 = { name: "Laura" };
    const Laura2 = { name: "Laura" };
    JSON.stringify(Laura1) === JSON.stringify(Laura2);
    // => true

    const Laura1 = { name: "Laura", age: 25 };
    const Laura2 = { age: 25, name: "Laura" };
    JSON.stringify(Laura1) === JSON.stringify(Laura2);
    // => false


We can use a deep equality. A deep equality is an equality based on what the object owns. Lodash provides isEqual to evaluate deep comparison.

const Laura1 = { name: "Laura", age: 25 };
const Laura2 = { age: 25, name: "Laura" };

JSON.stringify(Laura1) === JSON.stringify(Laura2);
// => false
isEqual(Laura1, Laura2);
// => true


Therefore, the isEqual lodash function is a wiser choice to compare the props.

export default memo(SidebarCard);
Before custom comparison
    
    const compareProps = (
      propsBefore: SidebarCardProps,
      propsAfter: SidebarCardProps
    ) => {
      return isEqual(propsAfter, propsBefore);
    };
    export default memo(SidebarCard, compareProps);
    
With custom comparison

Drum roll...

Profiler with not re-rendered third card

🥳 Yeah, got it! So for the 600 items list, when the second card is clicked on, here is the after memoization:

Profiler with not re-rendered card for 600 items

What if we only use memoize custom comparison without useCallback?

The isEqual on the onSetSelected function would have worked, the card wouldn’t have re-rendered. But it was a nice learning in case you have a function which is triggering a re-render.

Is there a difference between useMemo and useCallback?

useMemo is usually used for variable memoization. But you can use a useMemo to memoize a function.

React documentation:

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

Why not always use memoization?

Memoizing is not free, it costs memory and the comparison cost. It adds complexity to your code so it will need more efforts to read/to refactor.

Conclusion

Thanks to React.memo() HOC and memoization hooks, useless re-renders were avoided! I hope you've learned a bunch of things!

Go further:

React: Fantastic Hooks and How to Use Them

React-Virtualized: Why, When and How you should use it

Ivan Akulov’s thread on re-renders

Use React.memo() wisely

The Complete Guide to useRef() and Refs in React

Louise Loisel

Louise Loisel

Developer at Theodo