why does my event run multiple times in useEffect? - javascript

i could not understand why handler in useEffect runs multiple times, i know that my useEffect runs for the first time but i didn't understand why handler in useEffect runs multiple times.
useEffect(() => {
const keys = [];
const handler = (e) => {
keys.push(e.keyCode);
const getItemsFiltered = keys
.slice(0)
.slice(Math.max(keys.length - 3, 0))
.join("");
if (getItemsFiltered == 1716113) {
setShow(true);
}
console.log(getItemsFiltered);
if (getItemsFiltered == 1716114) {
document.getElementById("textField").focus();
}
};
document.addEventListener("keydown", handler, false);
return () => document.removeEventListener("keydown", handler, false);
}, []);

Your handler function reacts to every key press no matter where on the page you click! You can click in a section that's outside of your component, but your handler will still run because you attached your handler method as an event listener to the whole HTML page.
If you want to run the handler function only when you click on your component you can do the following:
document.getElementById("your-div-id").addEventListener("keydown", handler, false);
You can also use React's useRef hook to achieve the same effect.

Related

Can not remove firestore listener inside a function

My code sample looks like following:
useEffect(() => {
specialfunction();//listener for chat operations
}, []);
const specialfunction = async() => {
var mylistener = firebase.firestore()...onSnapshot((snapshot) => {
//do something with the docs retrieved
});
//my unlucky try to remove the listener after I leave the screen
return () => {
try{
mylistener();
console.log("LISTENER REMOVED")
}catch(error){console.log(error)};
}
};
Usually, if you want to remove a firestore listener, you just call the variable you attached it to, in my example 'mylistener()'.
Unluckily, my listener is not stopping. After I leave the screen and reenter it multiple times and receive a document, I notice that my listener fires multiple times.
I also can not stop it outside my 'specialfunction' because it is not a database listener where I just can call the ref again and stop it, its about a firestore listener here.
I also can not put the 'mylistener' outside of the 'specialfunction', it needs to stay inside.
Can you help me to stop the 'mylistener' after I leave the screen?
Not particularly well-versed in firebase but if you wished to remove event listener when dealing with useEffect hook, the template should be as below
useEffect(() => {
window.addEventListener("keyup", handleKeyUp)
return () => window.removeEventListener("keyup", handleKeyUp)
}, [collapsed, handleKeyUp])
Notice that in your useEffect hook you need to return a function which remove event listener
The key to this solution was another post but under a different topic.
Important to understand was that you need to predefine a variable and after that overwrite it with the firestore listener.
In this case you will be able to detach the listener in the return part of the 'useEffect'-Hook once you leave the screen, code looks like this now:
let mylistener;
useEffect(() => {
specialfunction();//listener for chat operations
return () => {
try{
mylistener(); //here you need to remove the listener
console.log("LISTENER REMOVED")
}catch(error){console.log(error)};
}
}, []);
const specialfunction = async() => {
var mylistener = firebase.firestore()...onSnapshot((snapshot) => {
//do something with the docs retrieved
});
};
Also, 'specialfunction' doesnt need to be async but I do async calls inside this function, its up to you.

Custom scroll behaviour?

Currently trying to lock scroll position after a single scroll for one second while I scroll down one section at a time. But I am having some unexpected behaviour.
const [nextSection, setNextSection] = useState('portfolio')
const [isScrollLocked, setIsScrollLocked] = useState(false)
const handleScroll = (section) => {
if (!isScrollLocked) {
console.log('ran', section)
setIsScrollLocked(true)
document.getElementById(section).scrollIntoView()
document.querySelector('body').classList.add('overflow-hidden')
setTimeout(() => {
document.querySelector('body').classList.remove('overflow-hidden')
setIsScrollLocked(false)
}, 1000)
}
}
useEffect(() => {
document.addEventListener('scroll', () => handleScroll(nextSection))
}, [nextSection])
Based on the code above I would think the conditional statement inside handleScroll could only run every second since I change it right away and then only change it back after the settimout but I get a lot of console logs with each scroll. I am updating the nextSection with a scroll spy and parsing it in but despite it being a dependency it does not always seem to update inside the event listener.
Because you added nextSection to your dependency list in your effect it will be called whenever nextSection changes and attach an additional handler to the scroll event. If an effect attaches a handler you need to return a function that detaches it again. Otherwise you will see problems like yours or memory leaks when the component unmounts:
useEffect(() => {
const handler = () => handleScroll(nextSection);
document.addEventListener('scroll', handler);
// cleanup callback, that will be called before the effect runs again
return () => document.removeEventListener('scroll', handler);
}, [nextSection])

setTimeout function in react causes type error

I have a form with a handle function attached to it.
The handle function has a timeout and this is causing some problems.
const timeOut = useRef(null);
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(e.target.value.toLowerCase().trim());
}, 500);
}
If I console.log(e.target.value) outside the settimeout function it works fine, when i incorporate the setTimeout function it breaks. Why is this?
I tried simplifying the function to just this :
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
setTimeout(() => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
}, 500);
}
The issue stays..It logs the first console.log and at the second it breaks.
Event values are cleared by react. You either need to use event.persist to persit event values or store the values from event to be used later
According to react documentation:
SyntheticEvent object will be reused and all properties will be
nullified after the event callback has been invoked. This is for
performance reasons. As such, you cannot access the event in an
asynchronous way.
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
const value = e.target.value.toLowerCase().trim();
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}
That’s because the e event object in react is a synthetic event object produced by react, not the native event object produced by browser internal.
In order to prevent allocation of new objects all the time, it’s designed to be a reusable object, which means its properties are stripped after emission and re-assigned for next event.
So for your case, because you revisited this object in async callback after emission, it’s been "recycled", making it’s properties outdated. To solve this problem, you can save up beforehand the desired value in the sync event loop, then pass it to async callback.
handleSearchChange = (e) => {
const value = e.target.value.toLowerCase().trim()
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}

setTimeout not clearing with React useEffect hook on mobile devices

Problem Summary: setTimeout's are not clearing on mobile devices when using React's useEffect hook. They are, however, clearing on desktop.
Problem Reproduction: https://codepen.io/amliving/pen/QzmPYE.
NB: run on a mobile device to reproduce the problem.
My Question: Why does my solution (explained below) work?
Details:
I'm creating a custom hook to detect idleness. Let's call it useDetectIdle. It dynamically adds and removes event listeners to window from a set of events, which when triggered call a provided callback after a period of time, via setTimeout.
Here is the list of events that will be dynamically added to and then removed from window:
const EVENTS = [
"scroll",
"keydown",
"keypress",
"touchstart",
"touchmove",
"mousedown", /* removing 'mousedown' for mobile devices solves the problem */
];
Here's the useDetectIdle hook. The import piece here is that this hook, when its calling component unmounts, should clear any existing timeout (and remove all event listeners):
const useDetectIdle = (inactivityTimeout, onIdle) => {
const timeoutRef = useRef(null);
const callbackRef = useRef(onIdle);
useEffect(() => {
callbackRef.current = onIdle;
});
const reset = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const id = setTimeout(callbackRef.current, inactivityTimeout);
timeoutRef.current = id;
};
useEffect(() => {
reset();
const handleEvent = _.throttle(() => {
reset();
}, 1000);
EVENTS.forEach(event => window.addEventListener(event, handleEvent));
return () => {
EVENTS.forEach(event => window.removeEventListener(event, handleEvent));
timeoutRef.current && clearTimeout(timeoutRef.current);
};
}, []);
};
useDetectIdle is called inside components like this:
const Example = () => {
useDetectIdle(5000, () => alert("first"));
return <div className="first">FIRST</div>;
};
On non-touchscreen devices, useDetectIdle works perfectly. But on mobile devices (both iOS and Android), any existing timeout is not cleared when its calling component unmounts. I.e. the callback passed to setTimemout still fires.
My Solution: Through some trial and error, I discovered that removing mousedown from the list of events solves the problem. Does anyone know what's happening under the hood?
Note: this doesn't answer "why your solution works", resp. why it seemed to help, but it points out 2 bugs in your code that I think are the real cause of the behavior. (I.e. your solution does not really work.)
You are handling _.throttle insufficiently - imagine the following scenario:
Your component with hook is mounted.
User triggers one of the events - throttled function is called, i.e. it just internally sets the timeout for 1000ms (throttled callback will be called at the end of 1000ms).
Before the timeout gets hit, you unmount the component. Listeners get removed just fine, but the internal timeout remained and will eventually fire your reset(), even though your component is already unmounted (and from there it will fire the idle callback after another inactivityTimeout ms).
Why the bug was prevalent on mobile was probably tied with what the user had to do to unmount the component on mobile vs desktop, the timing, and what events fired while doing it.
There is also the very tiny possibility that your component's DOM gets unmounted, and because React >= 17.x runs effect cleanup methods asynchronously, a timeout could fire just before your effect cleanup method. I doubt this would be consistently simulated but can be fixed too.
You can fix both issues by switching both effects to useLayoutEffect and introducing local variable unmounted:
const useDetectIdle = (inactivityTimeout, onIdle) => {
const timeoutRef = useRef(null);
const callbackRef = useRef();
useLayoutEffect(() => {
callbackRef.current = onIdle;
});
const reset = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const id = setTimeout(callbackRef.current, inactivityTimeout);
timeoutRef.current = id;
};
useLayoutEffect(() => {
reset();
let unmounted = false;
const handleEvent = _.throttle(() => {
if (!unmounted) reset();
}, 1000);
EVENTS.forEach(event => window.addEventListener(event, handleEvent));
return () => {
unmounted = true;
EVENTS.forEach(event => window.removeEventListener(event, handleEvent));
timeoutRef.current && clearTimeout(timeoutRef.current);
};
}, []);
};
PS: Idle callback after mount fires after inactivityTimeoutms, whereas subsequent callbacks after inactivityTimeout + 1000ms.

javascript debounce: how to get e.target?

I'm using the implementation of debounce from https://davidwalsh.name/javascript-debounce-function.
The question now is: How do I get the event (e.target) from the eventlistener and use it inside the debounced function?
This is what I've come up with:
document.querySelector('textarea')
.addEventListener('input', (e) => {
debounce(
() => { console.log('debounce at '+e.target.value); },
1000,
false
)(e); // add (e) so that the function debounce returns gets called inside the anonymous function
});
The problem is that it triggers immediately (and the debounce effect gets effectively killed).
you need to wrap the input event handler, like so:
function handler(e) {
console.log('debounce at '+e.target.value);
}
const debouncedHandler = debounce(handler, 1000)
document.querySelector('textarea').addEventListener('input',debouncedHandler, false)
now when the event fires it will call the same handler and not a new one
demo

Categories

Resources