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);
}, [])
Related
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
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])
useEffect(() => {
playLoop();
}, [state.playStatus]);
const playLoop = () => {
if (state.playStatus) {
setTimeout(() => {
console.log("Playing");
playLoop();
}, 2000);
} else {
console.log("Stopped");
return;
}
};
Output:
Stopped
// State Changed to true
Playing
Playing
Playing
Playing
// State Changed to false
Stopped
Playing // This is the problem, even the state is false this still goes on execute the Truthy stalemate
Playing
Playing
I am working on react-native and I want the recursion to stop when the state value becomes false.
Is there any other way I can implement this code I just want to repeatedly execute a function while the state value is true.
Thank you
Rather than having a playStatus boolean, I'd save the interval ID. That way, instead of setting playStatus to false, call clearInterval. Similarly, instead of setting playStatus to true, call setInterval.
// Can't easily use useState here, because you want
// to be able to call clearInterval on the current interval's ID on unmount
// (and not on re-render) (interval ID can't be in an old state closure)
const intervalIdRef = useRef(-1);
const startLoop = () => {
// make sure this is not called while the prior interval is running
// or first call clearInterval(intervalIdRef.current)
intervalIdRef.current = setInterval(
() => { console.log('Playing'); },
2000
);
};
const stopLoop = () => {
clearInterval(intervalIdRef.current);
};
// When component unmounts, clean up the interval:
useEffect(() => stopLoop, []);
The first thing you should do is make sure to clear the timeout when the state changes to stopped or otherwise check the state within the timeout callback function.
But the problem does not seem to be with the setTimeout code only by itself, but rather that this playLoop is also being called too many times. You should add a console.log with a timestamp right at the start of your playLoop to confirm or disprove this. And to find out where it is called from, you could use console.trace.
const playLoop = () => {
console.log(new Date(), ': playLoop called')
console.trace(); // optional
if (state.playSt....
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);
}
I am currently refactoring a react app from setState to hooks. I can't understand why the state variables aren't changed. Here is an example:
import React, { useState, useEffect } from 'react';
function Hook() {
const [num, setNum] = useState(1);
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, []);
const logNum = () => {
console.log(num);
}
const handleToggle = () => {
if (num == 1) {
console.log('setting num to 2');
setNum(2);
} else {
console.log('setting num to 1');
setNum(1);
}
}
return (
<div>
<button onClick={handleToggle}>TOGGLE BOOL</button>
</div>
);
}
export default Hook;
When i click the button, I was expecting the output to be something like:
// 1
// setting num to 2
// 2
// setting num to 1
// 1
But the output look like this:
Why is the updated num variable not logged?
Shouldn't the logNum() function always point to the current value of the state?
That's why effect dependencies have to be exhaustive. Don't lie about dependencies.
logNum closures over num, so on every rerender there is a new num variable containing the new value, and a new logNum function logging that value. Your effect however gets initialized only once, thus it only knows the first logNum. Therefore, you have to add logNum as a dependency, so that the effect gets updated whenever num and thus logNum changes:
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, [logNum]);
You'll notice that your effect does not correctly clean up, you should add a removeEventListener too.
return () => window.removeEventListener("mousemove", logNum);
Now if you debug this piece of code, you'll notice that the effect triggers on every rerender. That is because a new logNum function gets created on every rerender, no matter wether num changes or not. To prevent that, you can use useCallback to make the logNum reference stable:
const logNum = useCallback(() => console.log(num), [num]);
An alternative to all of this would be to use a reference to the current state:
const actualNum = useRef(num);
// that works no matter when and how this is executed
console.log(actualNum.current);