Improve search result click performance
Huh, dunno when I regressed this! Or maybe I never did it for search results, just the main items page? But we're needlessly re-rendering the entire search results list when you wear/unwear something, because `onRemove` always changes, and that breaks the `React.useMemo` on `Item`. Now, we cache the `onRemove` callback with `React.useCallback`, so perf is much happier!
This commit is contained in:
parent
571b064fb5
commit
6517087568
1 changed files with 91 additions and 56 deletions
|
@ -98,38 +98,30 @@ function SearchResults({
|
||||||
// Background we want to restore the previous one!
|
// Background we want to restore the previous one!
|
||||||
const [itemIdsToReconsider] = React.useState(outfitState.wornItemIds);
|
const [itemIdsToReconsider] = React.useState(outfitState.wornItemIds);
|
||||||
|
|
||||||
// When the checkbox changes, we should wear/unwear the item!
|
|
||||||
const onChange = (e) => {
|
|
||||||
const itemId = e.target.value;
|
|
||||||
const willBeWorn = e.target.checked;
|
|
||||||
if (willBeWorn) {
|
|
||||||
dispatchToOutfit({ type: "wearItem", itemId, itemIdsToReconsider });
|
|
||||||
} else {
|
|
||||||
dispatchToOutfit({ type: "unwearItem", itemId, itemIdsToReconsider });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// You can use UpArrow/DownArrow to navigate between items, and even back up
|
// You can use UpArrow/DownArrow to navigate between items, and even back up
|
||||||
// to the search field!
|
// to the search field!
|
||||||
const goToPrevItem = (e) => {
|
const goToPrevItem = React.useCallback(
|
||||||
const prevLabel = e.target.closest("label").previousSibling;
|
(e) => {
|
||||||
if (prevLabel) {
|
const prevLabel = e.target.closest("label").previousSibling;
|
||||||
prevLabel.querySelector("input[type=checkbox]").focus();
|
if (prevLabel) {
|
||||||
prevLabel.scrollIntoView({ block: "center" });
|
prevLabel.querySelector("input[type=checkbox]").focus();
|
||||||
e.preventDefault();
|
prevLabel.scrollIntoView({ block: "center" });
|
||||||
} else {
|
e.preventDefault();
|
||||||
// If we're at the top of the list, move back up to the search box!
|
} else {
|
||||||
onMoveFocusUpToQuery(e);
|
// If we're at the top of the list, move back up to the search box!
|
||||||
}
|
onMoveFocusUpToQuery(e);
|
||||||
};
|
}
|
||||||
const goToNextItem = (e) => {
|
},
|
||||||
|
[onMoveFocusUpToQuery]
|
||||||
|
);
|
||||||
|
const goToNextItem = React.useCallback((e) => {
|
||||||
const nextLabel = e.target.closest("label").nextSibling;
|
const nextLabel = e.target.closest("label").nextSibling;
|
||||||
if (nextLabel) {
|
if (nextLabel) {
|
||||||
nextLabel.querySelector("input[type=checkbox]").focus();
|
nextLabel.querySelector("input[type=checkbox]").focus();
|
||||||
nextLabel.scrollIntoView({ block: "center" });
|
nextLabel.scrollIntoView({ block: "center" });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// If the results aren't ready, we have some special case UI!
|
// If the results aren't ready, we have some special case UI!
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -167,38 +159,17 @@ function SearchResults({
|
||||||
<>
|
<>
|
||||||
<ItemListContainer>
|
<ItemListContainer>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<label key={item.id}>
|
<SearchResultItem
|
||||||
<VisuallyHidden
|
key={item.id}
|
||||||
as="input"
|
item={item}
|
||||||
type="checkbox"
|
itemIdsToReconsider={itemIdsToReconsider}
|
||||||
aria-label={`Wear "${item.name}"`}
|
isWorn={outfitState.wornItemIds.includes(item.id)}
|
||||||
value={item.id}
|
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||||
checked={outfitState.wornItemIds.includes(item.id)}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
ref={index === 0 ? firstSearchResultRef : null}
|
checkboxRef={index === 0 ? firstSearchResultRef : null}
|
||||||
onChange={onChange}
|
goToPrevItem={goToPrevItem}
|
||||||
onKeyDown={(e) => {
|
goToNextItem={goToNextItem}
|
||||||
if (e.key === "Enter") {
|
/>
|
||||||
e.target.click();
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
goToPrevItem(e);
|
|
||||||
} else if (e.key === "ArrowDown") {
|
|
||||||
goToNextItem(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
item={item}
|
|
||||||
isWorn={outfitState.wornItemIds.includes(item.id)}
|
|
||||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
|
||||||
onRemove={() =>
|
|
||||||
dispatchToOutfit({
|
|
||||||
type: "removeItem",
|
|
||||||
itemId: item.id,
|
|
||||||
itemIdsToReconsider,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</ItemListContainer>
|
</ItemListContainer>
|
||||||
{loadingMore && <ItemListSkeleton count={8} />}
|
{loadingMore && <ItemListSkeleton count={8} />}
|
||||||
|
@ -206,6 +177,70 @@ function SearchResults({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SearchResultItem({
|
||||||
|
item,
|
||||||
|
itemIdsToReconsider,
|
||||||
|
isWorn,
|
||||||
|
isInOutfit,
|
||||||
|
dispatchToOutfit,
|
||||||
|
checkboxRef,
|
||||||
|
goToPrevItem,
|
||||||
|
goToNextItem,
|
||||||
|
}) {
|
||||||
|
// It's important to use `useCallback` for `onRemove`, to avoid re-rendering
|
||||||
|
// the whole list of <Item>s!
|
||||||
|
const onRemove = React.useCallback(
|
||||||
|
() =>
|
||||||
|
dispatchToOutfit({
|
||||||
|
type: "removeItem",
|
||||||
|
itemId: item.id,
|
||||||
|
itemIdsToReconsider,
|
||||||
|
}),
|
||||||
|
[item.id, itemIdsToReconsider, dispatchToOutfit]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
<VisuallyHidden
|
||||||
|
as="input"
|
||||||
|
type="checkbox"
|
||||||
|
aria-label={`Wear "${item.name}"`}
|
||||||
|
value={item.id}
|
||||||
|
checked={isWorn}
|
||||||
|
ref={checkboxRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
const itemId = e.target.value;
|
||||||
|
const willBeWorn = e.target.checked;
|
||||||
|
if (willBeWorn) {
|
||||||
|
dispatchToOutfit({ type: "wearItem", itemId, itemIdsToReconsider });
|
||||||
|
} else {
|
||||||
|
dispatchToOutfit({
|
||||||
|
type: "unwearItem",
|
||||||
|
itemId,
|
||||||
|
itemIdsToReconsider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.target.click();
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
goToPrevItem(e);
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
goToNextItem(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
item={item}
|
||||||
|
isWorn={isWorn}
|
||||||
|
isInOutfit={isInOutfit}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useSearchResults manages the actual querying and state management of search!
|
* useSearchResults manages the actual querying and state management of search!
|
||||||
* It's hefty, infinite-scroll pagination is a bit of a thing!
|
* It's hefty, infinite-scroll pagination is a bit of a thing!
|
||||||
|
|
Loading…
Reference in a new issue