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
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:
Using useMemo:
Using batch:
Just wrap your child responsible for displaying the image with
React.memo
.Consider following example. If not
React.memo
, everyImage
component would re-render, when a new element is added to thearr
variable.Demo: https://codesandbox.io/s/naughty-bassi-2dtyf6?file=/src/App.js