How can I handle async loading ‘on the fly’ in React?

I’m writing an app which is essentially in React (it’s part of a framework, but React is the basis). The app needs to load a list from an API, and then display all the items from that list to the user.

The user can select up to 3 items from the list. When an item is selected, I load more information about that item, and display that to the user.

The issue I have is that the additional information which needs to be loaded requires quite a bit of processing before it can be displayed, so I want to store the result each time (so that if the user selects, for instance, Item 1, Item 2 and Item 3, and then deselects Item 2, and then reselects Item 2, it doesn’t have to reload and process the info for Item 2 all over again).

I have the following:

function App() {
  const [allItems, setAllItems] = React.useState(null);  // Name and id of every item
  const [itemsShown, setItemsShown] = React.useState([]); // id of shown items
  const [itemsInfo, setItemsInfo] = {};  // Full details of items

  // Initialise by getting list of items
  React.useEffect(() => {
    const getAll = async () => {
      const res = await Api.getItems();
      setAllItems(res);
    };

    getAll();
  }, []);

  const loadItem = async (itemId) => {
      // If item is already loaded, return without doing anything
      if(itemsInfo[itemId]) return;

      // Load item details - this function also processes the data to get it into
      //  the correct format
      const details = await Api.getItem(itemId);

      // Now save the processed data
      setItemsInfo({...itemsInfo, [itemId] : details})
      
  }

  // Handle the user changing the selection of items
  // This goes through the selector, and checks whether each item is selected
  // Those items that are selected are added to the 'itemsShown' variable
  const handleChangeSelection = (e) => {
    const newShown = [];
    for (let ii = 0; ii < e.target.options.length; ii++) {
      if (e.target.options[ii].selected && newShown.length < MAX_SELECTED) {
        // This item is selected, so add its id to shown items list
        newShown.push(e.target.options[ii].value);

        // If we don't have the full details for this item yet, load it
        if(!itemsInfo[e.target.options[ii].value]) {
            loadItem(e.target.options[ii].value)
        }
      }
    }
    setItemsShown([...newShown]);
  };

  // DISPLAY

  // Show a selector with the list of items as options
  // Whenever the selection changes, handleChangeSelection is called
  // Underneath that, show details of the selected items
  return (
    <div>
      <Selector
         options={allItems}
         onChange={handleChangeSelection}
      />
        {itemsShown.map((item) => (
           <ShowItem
              key={item.id}
              item={itemsInfo[item.id] ? itemsInfo[item.id] : null}
           />
          )}
    </div>
  );
}

export default App;

What I’m aiming for here is that the page initially loads all available items, getting the id and name for each one and displays them in a selector. When the user selects one or more items in the selector, the handleChangeSelection function loops over the selector to see what’s currently selected, and adds the id of each selected item to itemsShown. It also checks that we’ve already pulled in the full details of that item by checking for the key in itemsInfo; if not, it loads those details. Once they’re loaded, it updates itemsInfo with the full details, which are arranged in itemsInfo by key, so it will look like this:

{ key1 : {... item 1 details}, key2 : {... item 2 details}, etc... }

Meanwhile, the render method goes through all the itemsShown; for each one, it instantiates the ShowItem component; if the item details are known then it passes them in; otherwise it passes in null.

The problem I have is that the time taken for items to load varies, and sometimes they overlap. So it might start to load Item 1, and then start to load Item 2. Then Item 1 completes, and it updates the itemsInfo state variable to add item 1 details; meanwhile, before that’s complete, Item 2 loads, and updates the original itemsInfo variable (without Item 1 in it, because that hasn’t updated yet) to add Item 2.

I’m not sure how to handle this, because I don’t know when the user is going to click on an item, or how quickly they’re going to click. Technically they could click on an Item and then unselect it, but I still want the item to load because it will have started anyway.

How do I get this to work?