Parallel loading in React

17 April 2021History

software-development

Recently I had to build a React component that would merge the results of several independent requests into one set of data and render it as a single list.

The requests would look like this:

  • GET to load initial list of items
  • GET to load item 1 details
  • GET to load item 2 details
  • ... etc for each item in the list

I wanted the list to ultimately render like this:

  • Item 1 + details
  • Item 2 + details
  • ... etc for each item in the list

The problem was: how to load a list of items, then load details of each item, making a separate request per item, then combine all of this information and render it to the user, all within one component.

Synchronously combining results

The simplest way would be to await all the requests and then render them together at once.

Flowchart depicting synchronous loading of items
Flowchart depicting synchronous loading of items

Here is an implementation which uses Promise.all.

Codepen Link

function UsersAndStatuses(props) {
  const [users, setUsers] = React.useState([]);

  React.useEffect(async () => {
    const users = await mockUsers();
    const userIds = users.map((user) => user.id);

    // Promise.all technique
    const userStatuses = await Promise.all(userIds.map(mockUserStatus));

    const usersWithStatus = users.map((user, index) => ({
      ...user,
      ...userStatuses[index],
    }));

    setUsers(usersWithStatus);
  }, []);

  return (
    <ul>
      {!users.length && "Loading..."}
      {users.map((user) => (
        <li>
          {user.name} {user.status}
        </li>
      ))}
    </ul>
  );
}

The problem with the above is:

It could take a long time for all the requests to complete.

We don't want to keep the user waiting for the whole list to load before they can see any results.

It would be better if we could

  1. Load and quickly render the list of items without the details, then
  2. Load and render the detail for each item as soon as each response is received

Flowchart depicting parallel loading of items
Flowchart depicting parallel loading of items

Asynchronously combining results

Implementing this improved solution raised a challenge:

How to merge the details from all the requests together into one state variable without triggering a React refresh cycle?

If the React refresh cycle triggered, it would have caused the state variable to contain incomplete data, as one partial value would override another.

It turns out the solution is rather simple: we just have to re-use the latest copy of our state variable each time we set it.

So instead of the typical setState call:

setUsers({
  ...users,
  [updatedUserId]: updatedUser,
});

We pass a state setter whose parameter (currentUsers) will always have the last updated value:

setUsers((currentUsers) => ({
  ...currentUsers,
  [updatedUserId]: updatedUser,
}));

So... here's the parallel loading solution.

Codepen Link

function UsersAndStatuses(props) {
  const [usersById, setUsersById] = React.useState({});
  const users = React.useMemo(() => Object.values(usersById), [usersById]);

  React.useEffect(async () => {
    const usersList = await mockUsers();

    setUsersById(
      usersList.reduce(
        (acc, user) => ({
          ...acc,
          [user.id]: user,
        }),
        {}
      )
    );

    const userIds = usersList.map((user) => user.id);

    userIds.forEach(async (userId) => {
      const userStatus = await mockUserStatus(userId);

      // Async state setter technique
      setUsersById((currentUsersById) => ({
        ...currentUsersById,
        [userId]: {
          ...currentUsersById[userId],
          ...userStatus,
        },
      }));
    });
  }, []);

  return (
    <ul>
      {!users.length && "Loading..."}
      {users.map((user) => (
        <li>
          {user.name} {user.status}
        </li>
      ))}
    </ul>
  );
}
© 2024 Jonathan Conway