2

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?

Farid
  • 1,557
  • 2
  • 21
  • 35

1 Answers1

1

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>
  );
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Thanks for the help! I implemented all your changes, however, when I do `{console.log(images)}` right above the `
    ` tag, I still see the array of images in the console every time I select/unselect an image. Does that mean the component is rerendering every time?
    – Farid May 25 '20 at 13:36
  • yes, that will happen. As I said it because you are setting a state and a state change triggers a re-render – Shubham Khatri May 25 '20 at 13:39
  • You're right that a state changes causes a rerender, so is there a way to accomplish this (selecting/unselecting a single image) where the entire list of images is not rerendered? – Farid May 25 '20 at 15:32
  • You could avoid re-rendering of all the images by separating them out into a different component. However as I said, if you provider the key value correctly you do not need that optimization as its already done by react. React will only make changes to those images on dom which actually need change – Shubham Khatri May 25 '20 at 15:36
  • Yeah, I already added `key={title}` to both iterators. Just to verify that I understand what's happening here, when I see the `images` array being logged to the console every time, that means that the component is actually rerendering the list of images every time one is selected/unselected, correct? – Farid May 25 '20 at 15:50
  • Not really, what it means is that the component is going through a render phase where react will determine which image will need to be updated in DOM and will do so accordingly – Shubham Khatri May 25 '20 at 16:20
  • Ok. To be honest, I'm still not entirely sure how to verify that the entire list is not being rerendered every time (like somehow comparing "before" and "after" to show what's happening under the hood, which I thought console.log would help with), but hopefully the `key` attributes will take care of optimizations. I feel bad taking any more of your time. I appreciate your help with this! – Farid May 25 '20 at 16:31
  • I will suggest you to read the react doc about lifecycles and also about virtual dom for a deeper understanding – Shubham Khatri May 25 '20 at 16:36
  • i dont see how usecallback is helpful here if you render it inside an arrow function anyways? – adir abargil Aug 17 '21 at 15:07