I recently built a React component (called ItemIndexItem) which displays images on my application. For example, I have a Search component which will show an index of filtered Items. Each Item displayed is an ItemIndexItem component. Clicking an ItemIndexItem sends you to a ItemShow page where the same ItemIndexItem is employed.
Search.jsx
render() {
return (
<ul>
<li key={item.id}>
<div>
<Link to={`/items/${item.id}`}>
<ItemIndexItem src={item.photos[0].photoUrl} />
<p>${item.price}</p>
</Link>
</div>
</li>
...more li's
</ul>
)
}
ItemIndexItem.jsx
import React, { useState, useEffect } from "react";
export default function ItemIndexItem(props) {
const [imageIsReady, setImageIsReady] = useState(false);
useEffect(() => {
let img = new Image();
img.src = props.src;
img.onload = () => {
setImageIsReady(true);
};
});
if (!imageIsReady) return null;
return (
<div>
<img src={props.src} />
</div>
);
}
The component is working exactly as desired, except for a memory leak error thrown in console:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in ItemIndexItem (created by ItemShow)
in div (created by ItemShow)
For reference, this is the code inside ItemShow where I render the ItemIndexItem:
ItemShow.jsx
return (
...
<div>
<ul>
{this.props.item.photos.map(photo => {
return (
<li key={photo.photoUrl}>
<div>
<ItemIndexItem type='show' src={photo.photoUrl} />
</div>
</li>
);
})}
</ul>
</div>
...
I've tried utilizing a useEffect return function to set img
to null:
return () => img = null;
This does nothing, however. Since I don't create a subscription, there's not one to delete. So I think the problem is in the asynchronous nature of .onload
.
You're setting the state of a component which isn't mounted anymore. You could use the useRef
hook to determine if your component is still mounted or not, so for example:
function useIsMounted() {
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
and in your ItemIndexItem
...
export default function ItemIndexItem(props) {
const isMounted = useIsMounted();
const [imageIsReady, setImageIsReady] = useState(false);
...
img.onload = () => {
if (isMounted.current) {
setImageIsReady(true);
}
...
}
As described in the React documentation of useRef.
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
This means that you can use it to create references to HTML elements, but you can also place other variables inside of that ref, such as a boolean. In the case of my 'useIsMounted' hook, it sets it as mounted upon initialization, and sets it as unmounted when unmounting.
You're setting a state of a component no longer in the tree. I have a little hook utility to help with this scenario:
import { useCallback, useEffect, useRef } from 'react'
export const useIfMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
}, [])
const ifMounted = useCallback(
func => {
if (isMounted.current && func) {
func()
} else {
console.log('not mounted, not doing anything')
}
},[])
return ifMounted
}
export default useIfMounted
Which you can then use like this:
const ifMounted = useIfMounted()
//other code
img.onload = () => {
ifMounted(() => setImageIsReady(true))
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With