Remove Scroll EventListener After Firing Once - javascript

In my React project, I have a scroll event listener that checks when the user scrolls passed 590, setting state to true. This seems to work fine however I only want the function associated with the to run once so I'm adding removeEventListener as seen below, however the event continues to fire.
How do I appropriately remove this event listener?
useEffect(() => {
const handleNavSlide = () => {
if (window.scrollY > 590) {
setIsOpen(String(true))
setTimeout(() => {
setIsOpen(String(false))
}, 2000)
}
}
window.addEventListener("scroll", handleNavSlide)
return () => window.removeEventListener("scroll", handleNavSlide)
}, [])

This will happen because as per your dependency array which is [] here, your listener will only get removed when the component unmounts. For the once behaviour, change your implementation like so :-
useEffect(() => {
const handleNavSlide = () => {
if (window.scrollY > 590) {
setIsOpen(String(true))
setTimeout(() => {
setIsOpen(String(false))
}, 2000)
window.removeEventListener("scroll", handleNavSlide)
}
}
window.addEventListener("scroll", handleNavSlide)
}, [])

Related

RemoveEventListener doesn't work inside useEffect

I try to make lazy loading for my products list using React and Redux. The problem is that I can't removeEventListener after all products are loaded.
all_loaded tells me if are products are loaded (true) or not (false).
So after the all_loaded changed to true, useEffect run code inside else but eventListener still exist after that.
const { all_loaded } = useAppSelector((state) => state.productsSlice);
const bottomScrollDetection = () => {
const position = window.scrollY;
var limit = document.body.offsetHeight - window.innerHeight;
if (position === limit) {
dispatch(fetchProducts(true));
}
};
useEffect(() => {
dispatch(fetchProducts(false));
if (!all_loaded) {
document.addEventListener("scroll", bottomScrollDetection);
} else {
document.removeEventListener("scroll", bottomScrollDetection);
}
}, [all_loaded]);
On the next re-render, a new function will be affected to bottomScrollDetection, the removeEventListener call will not remove the initial listener.
You can use the cleanup function :
useEffect(() => {
if (!all_loaded) {
dispatch(fetchProducts(false));
document.addEventListener("scroll", bottomScrollDetection);
return () => document.removeEventListener("scroll", bottomScrollDetection);
}
}, [all_loaded]);

Canceling a timeout in useEffect() if user scrolls with react hooks

Basically the same question as How to cancel a javascript function if the user scrolls the page but using react hooks.
I wrote react code that scrolls down to the end of the page after 3 seconds.
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const id = setTimeout(() => scrollToEnd(), 3000)
return () => clearTimeout(id)
}, [])
I want modify this code so that if the user manually scrolls the page before this timeout, the timeout is cleared.
I was thinking of a solution like:
const [hasScrolled, setHasScrolled] = useState(false);
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const setHasScrolledCallback = () => setHasScrolled(true)
window.addEventListener("scroll", setHasScrolledCallback);
return () => window.removeEventListener("scroll", setHasScrolledCallback);
}, []);
useEffect(() => {
const scrollCallback = () => { if (hasScrolled) scrollToEnd() }
const id = setTimeout(scrollCallback, 3000)
return () => clearTimeout(id)
}, [])
This works, but I don't think this is the correct way to approach this problem, because the scroll event is fired multiple times, even after the timeout occurs. Also the scrollCallback isn't really canceled, it runs anyway even if it does nothing.

why i get different value width window using react js?

example:
resize using react js
this is my code:
import React, { useState, useEffect } from 'react';
const getWidthWindow = () => {
const [widthWindow, setWidthWindow] = useState(null)
const updateDimensions = () => {
setWidthWindow(window.screen.width)
}
useEffect(() => {
console.log(widthWindow)
setWidthWindow(window.screen.width)
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [widthWindow])
}
export default getWidthWindow;
I want to get the window width value but the result is like it doesn't match the window size so how to fix it?
Your code is correct but the logging isn't.
Add a hook to log the dimensions when it updates:
useEffect(() => {
console.log(windowDimensions)
}, [windowDimensions])
Working codesandbox.
I go with the above answer of adding windowDimensions to the useEffect's Dependency array but I like to add up little sugar on top of it..
On Resize, the event gets triggered continuously and impacts performance a bit..
So, I have implemented throttling to improve the performance..
Answer for your updated question: Stackblitz link
const GetWidthWindow = () => {
const [widthWindow, setWidthWindow] = useState(window.innerWidth);
useEffect(() => {
let throttleResizeTimer = null;
function handleResize() {
clearTimeout(throttleResizeTimer);
throttleResizeTimer = setTimeout(
() => setWidthWindow(window.innerWidth),
500
);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [widthWindow]);
return <p>{JSON.stringify(widthWindow)}</p>;
};
export default GetWidthWindow;
Answer for your old question:
useEffect(() => {
// implement throttle for little performance gain
let throttleResizeTimer = null;
function handleResize() {
clearTimeout(throttleResizeTimer);
throttleResizeTimer = setTimeout(
() => setWindowDimensions(getWindowDimensions()),
500
);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); }, [windowDimensions]);

Why my React component is still updating state even if I do cleanup in useEffect()?

I've got a little problem there and I dont know why my solution is not working properly.
Here is the code:
const [progress, setProgress] = useState(0);
useEffect(() => {
let isMounted = true;
if(isMounted === true) {
progress < 100 && setTimeout(() => setProgress(progress + 1), 20);
}
return () => isMounted = false;
}, [progress]);
Im doing there setTimeout async operation. Every 20ms i want to set state of progress by 1. Thats all. Also I added isMounted variable which contains state of component.
The problem is that when lets say, I mount this component and I unmount this
immediately after 1s maybe two then i dont get this error.
If I wait longer and unmount the component (before setTimeout has time to change the progress state to 100, which is the limit) then this error appears.
Why this error is appearing in such weird way?
Why does this error even appear when the component has clearly communicated when it is mounted and when not?
You need to either clear the timeout in the cleanup or use your isMounted variable within the timeout itself.
Clearing the timeout:
useEffect(() => {
let timeout;
if (progress < 100) {
timeout = setTimeout(() => {
setProgress(progress + 1)
}, 20);
}
return () => { clearTimeout(timeout) };
}, [progress]);
Using the isMounted variable:
useEffect(() => {
let isMounted = true;
if (progress < 100) {
setTimeout(() => {
if (isMounted) setProgress(progress + 1);
}, 20)
}
return () => { isMounted = false };
}, [progress]);

React useState doesn't update in window events

State does get set on the scroll, but logged from the eventlistener, it seems to be stuck at the initial value.
I guess it's something to do with scrolling being set when the side effect's defined, but how could I trigger a state change from a scroll otherwise? Same goes for any window event I presume.
Here's a codesandbox example: https://codesandbox.io/s/react-test-zft3e
const [scrolling, setScrolling] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
console.log(scrolling);
if (scrolling === false) setScrolling(true);
});
}, []);
return (
<>
scrolling: {scrolling}
</>
);
So your anonymous function is locked on initial value of scrolling. It's how closures works in JS and you better find out some pretty article on that, it may be tricky some time and hooks heavily rely on closures.
So far there are 3 different solutions here:
1. Recreate and re-register handler on each change
useEffect(() => {
const scrollHandler = () => {
if (scrolling === false) setScrolling(true);
};
window.addEventListener("scroll", scrollHandler);
return () => window.removeEventListener("scroll", scrollHandler);
}, [scrolling]);
while following this path ensure your are returning cleanup function from useEffect. It's good default approach but for scrolling it may affect performance because scroll event triggers too often.
2. Access data by reference
const scrolling = useRef(false);
useEffect(() => {
const handler = () => {
if (scrolling.current === false) scrolling.current = true;
};
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
return (
<>
scrolling: {scrolling}
</>
);
downside: changing ref does not trigger re-render. So you need to have some other variable to change it triggering re-render.
3. Use functional version of setter to access most recent value
(I see it as preferred way here):
useEffect(() => {
const scrollHandler = () => {
setScrolling((currentScrolling) => {
if (!currentScrolling) return true;
return false;
});
};
window.addEventListener("scroll", scrollHandler);
return () => window.removeEventListener("scroll", scrollHandler);
}, []);
Note Btw even for one-time use effect you better return cleanup function anyway.
PS Also by now you don't set scrolling to false, so you could just get rid of condition if(scrolling === false), but sure in real world scenario you may also run into something alike.
The event listener callback is only initialized once
This means that the variable at that moment are also "trapped" at that point, since on rerender you're not reinitializing the event listener.
It's kind of like a snapshot of the on mount moment.
If you move the console.log outside you will see it change as the rerenders happen and set the scroll value again.
const [scrolling, setScrolling] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
if (scrolling === false) setScrolling(true);
});
}, []);
console.log(scrolling);
return (
<>
scrolling: {scrolling}
</>
);
A solution that has personally served me well when I need to access a state (getState and setState) in an eventListener, without having to create a reference to that state (or all the states it has), is to use the following custom hook:
export function useEventListener(eventName, functionToCall, element) {
const savedFunction = useRef();
useEffect(() => {
savedFunction.current = functionToCall;
}, [functionToCall]);
useEffect(() => {
if (!element) return;
const eventListener = (event) => savedFunction.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
What I do is make a reference to the function to be called in the eventListener. in the component where I need an eventLister, it will look like this:
useEventListener("mousemove", getAndSetState, myRef.current); //myRef.current can be directly the window object
function getAndSetState() {
setState(state + 1);
}
I leave a codesandbox with a more complete code

Categories