Reputation: 43
I'm trying to convert a class component, which is a Gatsby Layout component, to a functional component using react hooks to manage state. My goal here is to open a modal once we reach a certain point in page and scroll up.
The problem I'm facing is that one prop I'm passing to this Layout component (TrustedInView, see code below), is changing value between renders, as console logs show it. So I'm confused about what's happening there. I expect the value of the prop to always be the same since the value logged in index.js is not changing (and not supposed to).
This is the code in the layout component :
// Hook
const usePrevious = value => {
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
};
let notOpenedYet = true;
// Layout component
const Layout = ({ intl, children, trustedInView }) => {
const [modalIsOpen, setOpen] = useState(false);
// Get the previous value (was passed into hook on last render)
let prevPosition = usePrevious(window.scrollY);
const handleNavigation = e => {
const custWindow = e.currentTarget;
if (prevPosition > custWindow.scrollY) {
console.log('trustedInView when goes up:', trustedInView);
if (trustedInView && notOpenedYet) {
notOpenedYet = false;
setTimeout(() => setOpen(true), 1500);
}
}
prevPosition = custWindow.scrollY;
};
useEffect(() => {
// Add event listener when component mounts:
window.addEventListener('scroll', e => handleNavigation(e));
// Remove event listener when component unmounts:
return window.removeEventListener('scroll', e => handleNavigation(e));
});
Here is the index.js render method with the layout component :
render() {
const { intl } = this.props;
const { activeVideo, tabs, tabNumber, trustedInView } = this.state;
const features = [
// some objects
];
console.log('trustedInView in index :', trustedInView);
return (
<Layout trustedInView={trustedInView}>
And this is the console logs :
I expect the value of the prop to always be the same
Can anyone set me on track?
Upvotes: 0
Views: 549
Reputation: 692
A new listener is being set up in every render and is never removed. So in subsequent scrolls, you are actually seeing more than one console.log per event, some of them have captured a different trustedInView
via the closure making it seem that the value is changing when it is not.
There are several problems with this code
useEffect
doesn't have any dependencies, so it's being called on every renderuseEffect
removeEventListener
is not really being called when the component unmounts, useEffect expected you to return a function and you are returning whatever the result of removeEventListener is.removeEventListener
instead of the function passed to addEventListener
another function created everytime.handleNavigation
so even if we were to fix the previous errors we have to make sure that we remove the correct listener. Luckily this is what useCallback
is for.Taking those things into account will look something like this:
const handleNavigation = useCallback(() => { /* ... */ }, [prevPosition, trustedInView])
useEffect(() => {
window.addEventListener('scroll', handleNavigation);
return () => {
window.removeEventListener('scroll', handleNavigation);
}
}, [handleNavigation])
But even this is problematic since the prevPosition
is going to change every time destroying the effect, plus is going to get stale for your purposes. So its better just to use a ref directly without your usePrevious
hook
const prevValueRef = useRef(0);
const handleNavigation = useCallback(e => {
const scrollY = e.currentTarget.scrollY;
const prevScrollY = prevValueRef.current;
// Save the value for the next event
prevValueRef.current = scrollY;
// ...
}, [trustedInView])
Upvotes: 1