Reputation: 244
I have some repeated hover states that runs a function to display some empty or filled icons (like you would see in some e-commerce websites with empty/filled carts). As practice, I wanted to create and put this into a custom hoverHooks component, w/ useRef
and useEffect
to run some add/remove
eventListeners, likeso:
const ref = useRef(null)
function enter() {
setHover(true)
}
function leave() {
setHover(false)
}
useEffect(() => {
ref.current.addEventListener('mouseenter',enter)
ref.current.addEventListener('mouseleave', leave)
return () => {
ref.current.removeEventListener('mouseenter',enter)
ref.current.removeEventListener('mouseleave',leave)
}
})
I did this so that the container holding my icons can just have the ref={ref}
without me having to repeatedly write onMouseEnter / onMouseLeave
. (I guess my refs are being repeated, but better three letters, and move my hover state
to just one place.
Cannot read property 'removeEventListener
' of null, is what I get. I read the React 17 docs regarding this, under "potential issues." But their suggestion isn't working (capturing the mutable data by storing it into a variable).
useEffect(() => {
const myRef = ref.current
myRef.current.addEventListener('mouseenter',enter)
myRef.current.addEventListener('mouseleave', leave)
return () => {
myRef.current.removeEventListener('mouseenter',enter)
myRef.current.removeEventListener('mouseleave',leave)
}
})
Any and all advice would be greatly appreciated! Thank you
Upvotes: 5
Views: 16300
Reputation: 5877
Instead of using ref.current
in the cleanup function:
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
Capture the ref
inside the effect.
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
Checkout the documentation for more details:
The problem is that someRef.current is mutable, so by the time the cleanup function runs, it may have been set to null. The solution is to capture any mutable values inside the effect:
Upvotes: 3
Reputation: 3546
I came across this recently in a test using jest and @testing-library/react. I was surprised none of automatic cleanup functions were handling this so I did some more digging and found an old git-issue which lead to this post in the react documentation (note, this is in reference to React v17.0 Release Candidate). To quote:
The problem is that
someRef.current
is mutable, so by the time the cleanup function runs, it may have been set to null. The solution is to capture any mutable values inside the effect:
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
Again, this was in regards to v17.0 release candidate so I don't suspect this to be a problem in later versions. I am using React v17.0.2 which leads me to believe this problem stems from our code and/or how we are using react-hooks with third-party libs.
I figured I'd share this information anyways in case someone runs across this problem and is looking for a solution that avoids memory leaks from not cleaning stranded event listeners. Cheers
Upvotes: 2
Reputation: 17
There is solution from YouTube:
const refHandle = useRef();
<i ref={refHandle}>
// This is to revise the original
refHandle.current.removeEventListener('touchstart', onStartTouchBegin, false);
// To the conditional format,
(ref.current)?.removeEventListener('touchstart', onStartTouchBegin, false);
Upvotes: 2
Reputation: 31495
Is that what you are looking for?
/* HOVER HOOK */
const useHover = ({ ref, onMouseEnter, onMouseLeave }) => {
React.useEffect(() => {
if (ref.current) {
ref.current.addEventListener('mouseenter',onMouseEnter);
ref.current.addEventListener('mouseleave',onMouseLeave);
}
return () => {
ref.current.removeEventListener('mouseenter',onMouseEnter);
ref.current.removeEventListener('mouseleave',onMouseLeave);
};
},[ref,onMouseEnter,onMouseLeave]);
return;
};
/* APP */
function App() {
const ref = React.useRef(null);
const onMouseEnter = () => console.log("ENTER");
const onMouseLeave = () => console.log("LEAVE");
useHover({ref,onMouseEnter,onMouseLeave});
return(
<div className="app" ref={ref}>
Hover me
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
.app {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Upvotes: 5