I have a list of images that render on page load via a map
function. I want to be able to select one or more images, and highlight them. Additionally, the selected images' titles will display at the top of the page. The issue I'm running in to is that the list seems to rerender every time I select an image. Here is the component:
export default function App() {
const [images, setImages] = React.useState<Image[]>([]);
const [selected, setSelected] = React.useState<boolean>(false);
const [imageTitles, setImageTitles] = React.useState<string[]>([]);
const handleImageSelection = (title: string, index: number) => {
setSelected(!selected);
const imageExists = imageTitles.indexOf(title) !== -1
if (!imageExists) {
setImageTitles([...imageTitles, title]);
} else {
setImageTitles(imageTitles.filter(str => str !== title));
}
}
return (
<div>
{console.log(images)} // The images array renders every time
<div>{imageTitles.map(title => <p>{title}</p>)}</div>
<section className="images">{
images.map(({ title, image}, index) =>
<img
style={{
marginBottom: 4,
border: imageTitles.indexOf(title) !== -1 ?
"4px solid blue" : "", // A selected image is highlighted
borderRadius: 16
}}
src={image}
alt={image}
onClick={() => handleImageSelection(title, index)}
/>
)
}</section>
</div>
);
}
I'm wondering if it's because I am changing the size of the imageTitles
array (and hence, the index
values) every time an image is selected/unselected.
I also tried useCallback
like so:
const handleImageSelection = React.useCallback((title: string, index: number) => {
setSelected(!selected);
const imageExists = imageTitles.indexOf(title) !== -1
if (!imageExists) {
setImageTitles([...imageTitles, title]);
} else {
setImageTitles(imageTitles.filter(str => str !== title));
}
}, [selected, imageTitles])
But it didn't seem to do the trick. My guess is because imagetitles
changes every time.
So, is it possible (for performance reasons) to avoid rerendering the list of images every time an image is selected/unselected?
Since you are adding selected and imageTitles in the dependency to useCallback, the useCallback will be recreated everytime it is called as it itself sets selected and imageTitles state
The solution here is to use setState callback pattern and pass empty array as dependency to useCallback
const handleImageSelection = React.useCallback((title: string, index: number) => {
setSelected(prevSelected => !prevSelected);
setImageTitles(prevImageTitles => {
const imageExists = prevImageTitles.indexOf(title) !== -1;
if (!imageExists) {
return [...prevImageTitles, title];
} else {
return prevImageTitles.filter(str => str !== title));
}
});
}, []);
Also note that the list will re-render everytime you set a state which is normal behavior. However react will optimize the rendering and will only re-render those elements which need change.
Another thing to note here is that you must pass on a key prop to mapped values in order for React to optimize rerendering further
return (
<div>
<div>{imageTitles.map(title => <p key={title}>{title}</p>)}</div>
<section className="images">{
images.map(({ title, image}, index) =>
<img
key={title} // any unique value here. If you don't have anything use index
style={{
marginBottom: 4,
border: imageTitles.indexOf(title) !== -1 ?
"4px solid blue" : "", // A selected image is highlighted
borderRadius: 16
}}
src={image}
alt={image}
onClick={() => handleImageSelection(title, index)}
/>
)
}</section>
</div>
);