How to Re-Render Large Lists Optimally When States Change in React

Using React, Recoil atom, selector, and hooks

In this article, we are going to look at how we can use Recoil’s atom, selector, and hooks to display and update a list of employees. The employees are either manager or non-manager. We will update the isManager field in the application state and see how only the selected column from the row is re-rendered. We will also derive a count element from the state and see how its UI is re-renderd automatically when the application state is updated.

Recoil documentation for atom states below.

An atom represents state in Recoil. The atom() function returns a writeable RecoilState object.

Note: all atom values are frozen.

Selectors represent a function, or derived state in Recoil. You can think of them as a “pure function” without side-effects that always returns the same value for a given set of dependency values.

As per Recoil, we need to wrap our application into RecoilRoot tag so that we can use Recoil state.

Components that use recoil state need RecoilRoot to appear somewhere in the parent tree

function App() {
  return (
    <RecoilRoot>
      <div className="App">
        <EmployeeList />
        <ManagerCount />
      </div>
    </RecoilRoot>
  );
}

Our data contains four record where two employees are managers and two are not. If the isManager field is true then the employee is manager.

[
  {
    "id": 1,
    "firstName": "E1 Anvi",
    "lastName": "E1 A",
    "isManager": true
  },
  {
    "id": 2,
    "firstName": "E2 Soumya",
    "lastName": "E2 A",
    "isManager": true
  },
  {
    "id": 3,
    "firstName": "E3 Anvi",
    "lastName": "E3 A",
    "isManager": false
  },
  {
    "id": 4,
    "firstName": "E4 Soumya",
    "lastName": "E4 A",
    "isManager": false
  }
]

Now we will create our application state using atom. The data object contains data from above screen shot.

const employeesDataState = atom({
  key: 'employeesDataState',
  default: data,
});

We will be using selector to derive the data from the employeeDataState. Also, selector has get and set methods.

  1. The getmethod will help us derive a piece of data from the application state. Here we will be deriving the complete state.
  2. In the setmethod we will be getting the updated record for an employee as parameter. We will be updating the employee list with this updatedEmployee record.

If only a get function is provided, the selector is read-only and returns a RecoilValueReadOnly object. If a set is also provided, it returns a writeable RecoilState object.

const employeesState = selector({
  key: 'employeesState',
  get: ({ get }) => {
    return get(employeesDataState)
  },
  set: ({ set }, updatedEmployee) => {
    set(employeesDataState, prevEmployees => {
      const pEmployees = _.clone(prevEmployees),
        employeeIndx = pEmployees.findIndex(emp => emp.id === updatedEmployee.id)
      pEmployees.splice(employeeIndx, 1, updatedEmployee)
      return [...pEmployees]
    })
  },
})

Now in our EmployeeList we just need to glue this selector data so that when the state is updated the list is also updated. For this we will be using the hook, useRecoilState, from the Recoil library.

useRecoilState returns a tuple where the first element is the value of state and the second element is a setter function that will update the value of the given state when called.

When the setEmployee is called it will make a call to the set method of the selector that will update the employeeDataState data. It will in return trigger the employeeState selector. And as this selector is being used in EmployeeList the list will be updated.

EmployeeList(setEmployee) -> employeeState(set method) -> employeeDataState -> employeeState(get method) -> EmployeeList

function EmployeeList() {
  const [employees, setEmployees] = useRecoilState(employeesState);
  return (
    <div className="employees">
      <h3>Employee list</h3>
      <table>
        <thead>
          <tr>
            {['FirstName', 'LastName', 'Manager'].map(
              header => <th key={header}>{header}</th>
            )}
          </tr>
        </thead>
        <tbody>
          {employees.map(employee =>
            <Employee
              key={employee.id}
              employee={employee}
              onUpdate={setEmployees}
            />
          )}
        </tbody>
      </table>
      <br />
    </div>
  );
}

Our Employee component is a memoized component which receives employee and setEmployee function as parameter.

In our Employee component we will be displaying a table with employee firstNamelastName and if employee is a manager or not. The last column will contain radio items which we can toggle. When the radio items are clicked, a call is made to the setEmployee method which will trigger the update in our application state.

const Employee = React.memo(EmployeeComp);

function EmployeeComp({ employee, onUpdate }) {
  const updateHandler = e => onUpdate({
    ...employee, isManager: e.target.value
  })
  return (
    <tr>
      <td>{employee.firstName}</td>
      <td>{employee.lastName}</td>
      <td>
        <RadioGroup defaultValue={employee.isManager}
          onChange={updateHandler} buttonStyle="solid">
          <RadioButton value={true}>Yes</RadioButton>
          <RadioButton value={false}>No</RadioButton>
        </RadioGroup>
      </td>
    </tr>
  );
}

We also have a component ManagerCount that will just display the count of managers in the list. First we will create a selector which will return the manager count from the application state. In the managerSelector below, we just have a get a method which filter’s and returns the employees that are managers.

const managerSelector = selector({
  key: 'managerCountSelector',
  get: ({ get }) => get(employeesState)
    .filter(employee => employee.isManager),
});

Now in our ManagerCount component we just want to read the value from the selector so we will use the hook useRecoilValue.

useRecoilValue returns the value of the given Recoil state. This hook will implicitly subscribe the component to the given state.

function ManagerCount() {
  const managers = useRecoilValue(managerSelector);
  return <>Manager Count: {managers.length}</>;
}

Great, our application is ready. Let’s see it in action. So here we have two employees as managers and the remaining two are not managers.

Employee List
Now, let’s toggle the second employee from manager to non-manager.

Here we observe a couple of things:

  1. The complete list does not re-render. Only the second row is re-rendered. That’s great news. So even if we have a large list the performance of the application will be fast.
  2. When we update the employee record, the ManagerCount component also updates seamlessly and there is change in count from two to one. Here you can see how we are deriving only the manager records from the application state using selector and how Recoil automatically re-renders the ManagerCount component when the actual application state is changed.

Conclusion

It seems that Recoil is a great library that compliments React perfectly when it comes for application’s state management. The ease of using it and it’s effortless integration with React makes it a great choice and definite contender when it comes to application’s state management.