Search code examples
onclickreact-hooksmemoizationmap-functionusecallback

Avoid rerendering a list in React Hooks


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?


Solution

  • 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>
      );