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?
2
Answers
To handle the asynchronous loading and updating of item details without overlapping or losing data, you can implement a solution using useEffect hooks for each item’s loading process. This ensures that each item’s details are processed and stored independently of each other. Additionally, you can use a loading state to prevent the display of incomplete item details.
Here’s how you can modify your loadItem function and rendering logic to handle the asynchronous loading more robustly:
Try to add async await