React Hooks and Memo: Optimally Update and Render a Large List

We use React so that it can optimally update the DOM for us using it’s magic. But understanding and using React correctly is of great importance as then only we can gain its benefits else we are stuck with performance problem for our application.

In this article we will be going through a scenario where, on update, we will be re-rendering a list of employees using:

  • useCallback — Returns a memoized callback
  • React.memo — React.memo is a higher order component. It’s is used for functions instead of class. 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.

Let’s start

So initially, App component will be our container that has the employee data. It will render employees in a table. Each row in the table will render individual employee’s id, first name and last name. The first name and last name are editable from UI.

Employee List
Our React UI displaying the data in table. Each row has employee ID, first name and last name where firstname and last name are textfields.
function App() {
  const [employees, setEmployees] = useState(data);

  const onChange = updatedEmployee => {
    const employeeIndx = employees.findIndex(emp => emp.id === updatedEmployee.id);
    employees[employeeIndx] = updatedEmployee;
    setEmployees([...employees]);
  }

  return (
    <div className="App">
      <h3>Employee list</h3>
      <table>
        <thead>
          <tr>
            <th>Emp Id</th>
            <th>FirstName</th>
            <th>LastName</th>
          </tr>
        </thead>
        <tbody>
        {
          employees.map(employee => 
          <EmployeeComp
            key={employee.id} 
            employee={employee}
            onUpdate={onChange} />)
        }
        </tbody>
      </table>
      <br/>
    </div>
  );
}
function EmployeeComp({ employee, onUpdate }) {
  const updateHandler = data => onUpdate({...employee, ...data})
  console.log(`${employee.id}`)
  return (
    <tr>
      <td>{employee.id}</td>
      <td>
        <Input
          value={employee.firstName}
          onChange={(e) => updateHandler({firstName: e.target.value})} />
      </td>
      <td>
      <Input
          value={employee.lastName} 
          onChange={(e) => updateHandler({lastName: e.target.value})} />
      </td>
    </tr>
  );
}
[{
	"id": 1,
	"firstName": "E1 Anvi",
	"lastName": "E1 A"
}, {
	"id": 2,
	"firstName": "E2 Anvi",
	"lastName": "E2 A"
}, {
	"id": 3,
	"firstName": "E3 Anvi",
	"lastName": "E3 A"
}, {
	"id": 4,
	"firstName": "E4 Anvi",
	"lastName": "E4 A"
}, {
	"id": 5,
	"firstName": "E5 Anvi",
	"lastName": "E5 A"
}]

Test Performance

Let’s test the performance of the Application. For this you will need an chrome extension React Developer Tools. After installation we can open Developer Tools in chrome using cmd+shift+I(mac) or control+shift+i(windows). We need to check the “Highlight updates when components render” checkbox from Components tab. This will basically highlight all components that are being re-rendered when we update the employee list.

Component tab in dev tools
Component tab in dev tools
Now let’s update the firstname of an employee and see how the application and components re-render.

What is happening over here? If I update the first employee’s first name why does the complete list gets re-rendered? Lets’ get back to our App code and see what is the problem.

When we update any employee’s first name the setEmployees function updates the state of the App component triggering the App container to re-render. This App re-render causes all EmployeeRow components to re-render even if only one row is changed. This is the perfect usecase where React.memo fits. When we wrap a component using React.memo it memoizes it depending on the props value, the component will only be re-rendered if the props value change. In React.memo, the props comparison is shallow and if our props object is complex then we can pass a comparison function as second parameter. Though i would suggest to keep the props object as simple as possible.

{
          employees.map(employee => 
          <Employee
            key={employee.id} 
            employee={employee}
            onUpdate={onChange} />)
        }
        </tbody>
      </table>
      <br/>
    </div>
  );
}

const Employee = React.memo(EmployeeComp)

function EmployeeComp({ employee, onUpdate }) {
  const updateHandler = data => onUpdate({...employee, ...data})

Great let’s see if the performance is improved.

Emp Id is not re-rendering now but FirstName and LastName is being re-rendered for all employees

You can see that the Emp Id column is not being re-rendered but still the FirstName and LastName columns for each Row are being re-rendered. Hmm, let’s see what is still missing.

Ok so the problem is our onChange function reference. Whenever any update is made the App is re-render, so while this happens on each re-render the onChange variable get a new function. As onChange is also passed as props to EmployeeComp component and React.memo update depends on props, each row is re-rendered. Here useCallback hook comes to our rescue as it will memoize the callback and return us the same reference until there is any change in it’s dependencies. We will be passing empty array as second parameter to useCallback, that means no dependencies to the useCallback function so the onChange reference will never change. Now re-render to EmployeeComp will only happen when we employee data is changed.

function App() {
  const [employees, setEmployees] = useState(data);

  const onChange = React.useCallback(updatedEmployee => {
    const employeeIndx = employees.findIndex(emp => emp.id === updatedEmployee.id);
    employees[employeeIndx] = updatedEmployee;
    setEmployees([...employees]);
  }, [])
  
  return (

Cool, lets see our performance now.

Now only the firstname and lastname for first employee is being re-rendered

The performance is greatly optimised and now only single row is being re-rendered instead of the complete list. That’s so cool.

Is they any other problem with the code? Lets see, right we can change the update logic of onChange function. The setEmployees function updates the state of the application. Now these calls are optimized and made asynchronously by React. So suppose there is a case where two or more entities try to call onChange there we might get a race condition and we won’t be sure which happens when making one of our calls work on stale data. To make sure you all your calls are working on the correct previous state instead of passing an array we can pass a function to setEmployees. Below code guarantees that we are working on the correct previous state of the application.

const onChange = React.useCallback(updatedEmployee => {
    setEmployees(prevEmployees => {
      const employeeIndx = prevEmployees.findIndex(emp => emp.id === updatedEmployee.id);
      prevEmployees[employeeIndx] = updatedEmployee;
      return [...prevEmployees];
    })
  }, [])

Conclusion

By understanding how React and hooks works we can definitely achieve an performant application. Also React Developer Tool is a great extension and a must have if we are working on React project. It can easily help us identify re-rendering problems. It has other capabilities too but i just needed the render highlight feature as of now.

The reason for this article was to get some understand on how React rendering and hooks work. Specially in cases where we are rendering and updating a list of items. Think what would have happened if we had a long list with more columns and the optimisation weren’t there. Even a simple update would make the Application feel so slow. But now with these small and simple changes your application will be fast.

Hope you like this article 🙂