skip to Main Content

Im having an issue with my react component where my item list re-renders EVERY time a new image is added.

Here is my useEffect that I use to load image urls from the database:

const [imageUrls, setImageUrls] = useState({});

  // Effect to fetch image URLs and store them in the state
  useEffect(() => {
    const getImageUrls = async () => {
      const urls = {};

      for (const [index, dataItem] of selectedData.entries()) {
        const { question, parts } = dataItem;
        let questionUrls = [];

        // Loop to fetch images for each part of the question
        for (let i = 1; i <= parts; i++) {
          const imageName = `${question}${i}`;
          const { data, error } = await supabase.storage
            .from('ocr-questions')
            .createSignedUrl(`football/images/${imageName}`, 2 * 60 * 1000);

          if (!error) {
            questionUrls.push({ url: data, isFirstImage: i === 1, imageNumber: index + 1 });
          }
        }

        urls[dataItem.id] = questionUrls;
      }

      // Merge the newly fetched URLs with the existing ones in the state
      setImageUrls((prevImageUrls) => ({ ...prevImageUrls, ...urls }));
    };

    getImageUrls();
  }, [selectedData]);

The selectedData is passed from the parent component – where the user can select options and they will be displayed with this component. The images are displayed like this:

<Box sx={{ p: 2 }}>
          {selectedData.length > 0 ? (
            <List>
              {selectedData.map((dataItem, index) => (
                <ListItem key={dataItem.id}>
                  <ListItemText
                    primary={
                      <>
                        {imageUrls[dataItem.id] ? (
                          imageUrls[dataItem.id].map((imageUrl, urlIndex) => (
                            <div key={urlIndex}>
                              {imageUrl.isFirstImage && (
                                <Typography variant="body2">
                                  Question {imageUrl.imageNumber}
                                </Typography>
                              )}
                              <ResponsiveImage
                                src={imageUrl.url.signedUrl}
                                alt={`${dataItem.question} Part ${urlIndex + 1}`}
                              />
                            </div>
                          ))
                        ) : (
                          <QuestionLoadingContainer>
                            <CircularProgress size={40} />
                          </QuestionLoadingContainer>
                        )}
                      </>
                    }
                    secondary={`Paper: ${dataItem.paper}, Score: ${dataItem.score}`}
                  />
                </ListItem>
              ))}
            </List>
          ) : (
            <Typography
              variant="body1"
              color="text.secondary"
            >
              No images selected.
            </Typography>
          )}
        </Box>

How can we prevent this re-rendering issue every time a user adds an image? The ideal outcome is the user adds an image and the whole list doesn’t re-render.

Update

  • Following the response from Bilal I have tried both batch and useMemo :

    const memoizedSelectedData = useMemo(() => selectedData, [selectedData]);
    useEffect(() => {
    const getImageUrls = async () => {
    const urls = {};
    for (const [index, dataItem] of memoizedSelectedData.entries()) {
    const { question, parts } = dataItem;
    let questionUrls = [];

         // Loop to fetch images for each part of the question
         for (let i = 1; i <= parts; i++) {
           const imageName = `${question}${i}`;
           const { data, error } = await supabase.storage
             .from('ocr-questions')
             .createSignedUrl(`computer-science/A2/${imageName}`, 2 * 60 * 1000);
    
           if (!error) {
             questionUrls.push({ url: data, isFirstImage: i === 1, questionNumber: index + 1 });
           }
         }
    
         urls[dataItem.id] = questionUrls;
       }
    
       // Set the image URLs in the state
       setImageUrls((prevImageUrls) => ({ ...prevImageUrls, ...urls }));
     };
    
     getImageUrls();
    

    }, [memoizedSelectedData]);

here is the implementation from kind user:

const ResponsiveImage = React.memo(({ imageUrl, urlIndex, imageNumber, image }) => (
    <div key={urlIndex}>
      {imageUrl.isFirstImage && <Typography variant="body2">Image {imageNumber}</Typography>}
      <img
        src={imageUrl.url.signedUrl}
        alt={`${image} Part ${urlIndex + 1}`}
        style={{
          width: '100%',
          height: 'auto',
          maxWidth: '100%',
          display: 'block',
        }}
      />
    </div>
  ));

Render:

<List>
              {selectedData.map((dataItem, index) => (
                <ListItem key={dataItem.id}>
                  <ListItemText
                    primary={
                      <>
                        {/* Loop to render each image for the question */}
                        {imageUrls[dataItem.id] ? (
                          imageUrls[dataItem.id].map((imageUrl, urlIndex) => (
                            <ResponsiveImage
                              key={urlIndex}
                              imageUrl={imageUrl}
                              urlIndex={urlIndex}
                              questionNumber={imageUrl.questionNumber}
                              question={dataItem.question}
                            />
                          ))
                        ) : (
                          <QuestionLoadingContainer>
                            <CircularProgress size={40} />
                          </QuestionLoadingContainer>
                        )}
                      </>
                    }
                    secondary={`Paper: ${dataItem.paper}, Marks: ${dataItem.marks}`}
                  />
                </ListItem>
              ))}
            </List>

Third Update with primitives as recommended by kind user

const ResponsiveImage = React.memo(
    ({ signedUrl, isFirstImage, urlIndex, questionNumber, question }) => (
      <div key={urlIndex}>
        {isFirstImage && <Typography variant="body2">Question {questionNumber}</Typography>}
        <img
          src={signedUrl}
          alt={`${question} Part ${urlIndex + 1}`}
          style={{
            width: '100%',
            height: 'auto',
            maxWidth: '100%',
            display: 'block',
          }}
        />
      </div>
    )
  );

Render:

<ListItem key={dataItem.id}>
                  <ListItemText
                    primary={
                      <>
                        {/* Loop to render each image for the question */}
                        {imageUrls[dataItem.id] ? (
                          imageUrls[dataItem.id].map((imageUrl, urlIndex) => (
                            <ResponsiveImage
                              key={urlIndex}
                              signedUrl={imageUrl.url.signedUrl}
                              isFirstImage={imageUrl.isFirstImage}
                              urlIndex={urlIndex}
                              questionNumber={imageUrl.questionNumber}
                              question={dataItem.question}
                            />
                          ))
                        ) : (
                          <QuestionLoadingContainer>
                            <CircularProgress size={40} />
                          </QuestionLoadingContainer>
                        )}
                      </>
                    }
                    secondary={`Paper: ${dataItem.paper}, Marks: ${dataItem.marks}`}
                  />
                </ListItem>

Output of logs

console.log('Type of signedUrl:', typeof signedUrl);
      console.log('Type of isFirstImage:', typeof isFirstImage);
      console.log('Type of urlIndex:', typeof urlIndex);
      console.log('Type of questionNumber:', typeof questionNumber);
      console.log('Type of question:', typeof question);
Type of signedUrl: string
Type of isFirstImage: boolean
Type of urlIndex: number
Type of questionNumber: number
Type of question: string

For:

const ResponsiveImage = React.memo(
    ({ signedUrl, isFirstImage, urlIndex, questionNumber, question }) => {
      console.log('Type of signedUrl:', typeof signedUrl);
      console.log('Type of isFirstImage:', typeof isFirstImage);
      console.log('Type of urlIndex:', typeof urlIndex);
      console.log('Type of questionNumber:', typeof questionNumber);
      console.log('Type of question:', typeof question);

      return (
        <div key={urlIndex}>
          {isFirstImage && <Typography variant="body2">Question {questionNumber}</Typography>}
          <img
            src={signedUrl}
            alt={`${question} Part ${urlIndex + 1}`}
            style={{
              width: '100%',
              height: 'auto',
              maxWidth: '100%',
              display: 'block',
            }}
          />
        </div>
      );
    }
  );

These solutions have not worked and the list still re-renders

2

Answers


  1. The re-rendering issue you are experiencing might be related to how the state is being updated within the getImageUrls function and how the selectedData array is managed.

    To resolve this you can either use batch or useMemo:

    1. Using useMemo:

      const memoizedSelectedData = useMemo(() => selectedData, [selectedData]);
      
    2. Using batch:

      import { batch } from 'react-dom';
      
      useEffect(() => {
          const getImageUrls = async () => {
              const urls = {};
              for (const [index, dataItem] of selectedData.entries()) {
                  urls[dataItem.id] = questionUrls;
              }
              batch(() => {
                  setImageUrls((prevImageUrls) => ({ ...prevImageUrls, ...urls }));
              });
          };
          getImageUrls();
      }, [selectedData]);
      
    Login or Signup to reply.
  2. Avoid re-rendering whole list when images are added in React

    Just wrap your child responsible for displaying the image with React.memo.

    Consider following example. If not React.memo, every Image component would re-render, when a new element is added to the arr variable.

    const Image = React.memo(({ id }) => {
      console.log(id, " re rendered");
      return <div>this is image with id {id}</div>;
    });
    
    export default function App() {
      const [arr, setArr] = React.useState([]);
    
      React.useEffect(() => {
        const interval = setInterval(() => {
          setArr((prev) => [...prev, prev.length + 1]);
        }, 2000);
    
        return () => {
          window.clearInterval(interval);
        };
      }, []);
    
      return (
        <>
          {arr.map((item) => (
            <Image key={item} id={item} />
          ))}
        </>
      );
    }
    

    Demo: https://codesandbox.io/s/naughty-bassi-2dtyf6?file=/src/App.js

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search