Custom scroll behaviour? - javascript

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])

Related

React won't reload function despite state change

I have a state that tracks the window width:
const [innerWidth, setInnerWidth] = useState(window.innerWidth)
In useEffect, I create a resize eventListener which sets the state to the new width:
useEffect(() => {
document.addEventListener('resize', () => {
setInnerWidth(window.innerWidth)
})
}, [])
Lastly, I have a function test that logs the innerWidth every 5 seconds, with an interval started in useEffect
function test() {
console.log(innerWidth)
}
useEffect(() => {
setInterval(test, 5000)
}, [])
Unfortunately, despite any resize that happen, the test() function keeps on logging the original innerWidth value.
How can I tell react to reload the test function as well?
EDIT:
The perpetual log of the innerWidth was just a simplification of my actual use case. Actually, the timer is shifting an element on the x-axis, and I need to know when it exceeds the width to stop the execution and start again.
Creating and invalidating a loop every time the window changes, like in several answers you've given, temporarily stops the shifting of my element, as the loop gets invalidated. I would like to avoid this.
The useEffect created a closure around the original values, so that's all it ever logs. You'd need the effect to update any time the value changes, by adding it to the dependency array:
useEffect(() => {
setInterval(test, 5000)
}, [innerWidth])
This would of course create a new interval on every state change. So the useEffect should return a function which cancels the interval:
useEffect(() => {
const x = setInterval(test, 5000);
return () => clearInterval(x);
}, [innerWidth])
That way there's only one interval running at any given time.
Though this begs the question... Why? If the goal is to log the value of innerWidth to observe its changes, then why re-log the same value every 5 seconds indefinitely? Skip the test function and the interval entirely and just log the value any time it changes:
useEffect(() => {
console.log(innerWidth);
}, [innerWidth])
Can you change the test function to an anonymous function?
const test = () => {
console.log(innerWidth);
};
Change you useEffect:
useEffect(() => {
window.addEventListener('resize', () => {
setInnerWidth(window.innerWidth);
});
}, [setInnerWidth]);
The solution was wrapping the innerWidth into an object, so that it is passed by reference and it 'updates' in the test function.
const innerWidthWrapper = {width: window.innerWidth}
useEffect(() => {
innerWidthWrapper.width = window.innerWidth
})
}, [])
Edit: Your issue using the interval function is explained in this answer
This code works for me by logging the state variable using the effect hook:
const [innerWidth, setInnerWidth] = useState(window.innerWidth);
useEffect(() => {
const updateWidth = () => {
setInnerWidth(window.innerWidth);
};
window.addEventListener("resize", updateWidth);
}, []);
useEffect(() => {
console.log(innerWidth);
}, [innerWidth]);
I added the eventlistener using the window object.
Sandbox

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.

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);
}

How to get updated state value in React useEffect hook

I'm listening to a scroll event in a function for a pagination feature. The scroll event is called inside a useEffect hook. When the scroll event is fired, I want to make a HTTP call with a value already set in state, and update that value if the HTTP request is successful.
However, whenever the event fires, it only uses the initial value of the item in state, even though the value has been updated, and I can see the updated value outside of the function.
How do I get the updated value inside the onScroll callback?
Below is a code snippet
const [test, setTest] = useState(0);
// The below commented out code works because it's not inside useEffect
// window.addEventListener('scroll', () => requestAnimationFrame(() => {
// console.log(test);
// setTest(test + 1);
// }));
useEffect(() => {
window.addEventListener('scroll', () => requestAnimationFrame(() => {
console.log(test); // this always returns 0
setTest(test + 1);
}));
}, [])
console.log(test); // this returns the updated/correct value.
By providing an empty array as the second argument to the effect, you've told react to never recreate this effect. So it happens just once, and the variables it has in its closure are never going to change.
Option 1:
You can get the effect to rerun by either removing the dependency array, or populating it with the variables you care about. Since this will result in the effect running multiple times you will also need to provide a cleanup function for tearing down the previous effect.
useEffect(() => {
const callback = () => requestAnimationFrame(() => {
setTest(test + 1);
})
window.addEventListener('scroll', callback);
return () => window.removeEventListener('scroll', callback);
}) // <------
Option 2:
Since the only thing you need to be refreshed is the latest value of state, you can use the callback version of setTest. React will pass you in the latest value, and you can compute the change based on that.
You will still want to have a teardown function so that the listener can be removed if this component unmounts:
useEffect(() => {
const callback = () => requestAnimationFrame(() => {
setTest(prev => prev + 1); // <------
})
window.addEventListener('scroll', callback);
return () => window.removeEventListener('scroll', callback);
}, [])

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.

Categories

Resources