How to manage asynchronous state updates when using event handlers in render method? - javascript

Let me explain the goal of my code first. I have a react component called "Tile" containing a sub-component called "TileMenu" which shows up when I make a right click on my Tile, calling the function "openMenu". I wanted to have two ways of closing it:
clicking somewhere else
waiting some time
But, I also wanted it to stay in place if the mouse was over it. So I needed a function to cancel the timer, which I called "keepMenuOpened". If I moved my mouse away, openMenu() was called again to relaunch the timer.
Here is my code:
import TileMenu from './TileMenu'
function Tile() {
const [openedMenu, setOpenedMenu] = useState(false);
// state used to display —or not— the TileMenu component
const [timeoutID, setTimeoutID] = useState(null);
// state to store timeout ID and clear it
function openMenu() {
// Actually open TileMenu
setOpenedMenu(true);
// Prepare TileMenu closing
window.onclick = closeMenu;
// first case: click somewhere else
setTimeoutID(setTimeout(closeMenu, 3000));
// second case: time out
console.log('open', timeoutID);
}
function closeMenu() {
setOpenedMenu(false);
window.onclick = null;
console.log('close', timeoutID);
clearTimeout(timeoutID);
}
function keepMenuOpened() {
console.log('keep', timeoutID);
clearTimeout(timeoutID);
}
return(
<>
{openedMenu &&
<TileMenu
onMouseOver={keepMenuOpened} onMouseLeave={openMenu} // These two props are passed on to TileMenu component
/>}
<textarea
onContextMenu={openMenu}
>
</textarea>
</>
);
}
export default Tile
At first, it seemed to work perfectly. But I noticed that when I opened, then closed manually, and finally opened my TileMenu again, the delay it took to close a second time (this time alone) was calculated from the first time I opened it.
I used console.log() to see what was happening under the hood and it seemed to be caused by the asynchronous update of states in React (Indeed, at the first attempt, I get open null and close null in the console. When I move my mouse over the TileMenu and then leave it, I get for example open 53, then keep 89 and then open 89 !) If I understand well my specific case, React uses the previous state in openMenu and closeMenu but the current state in keepMenuOpened.
In fact, this is not my first attempt and before using a react state, "timeoutID" was a simple variable. But this time, it was inaccessible inside keepMenuOpened (it logged keep undefined in the console) even if declared in Tile() scope and accessible in openMenu and closeMenu. I think it's because closeMenu is called from openMenu. I found on the net it was called a closure but I didn't figure out exactly how it worked with React.
And now I haven't figured out how to solve my specific problem. I found that I could use useEffect() to access my updated states but it doesn't work in my case where I need to declare my functions inside Tile() to use them as event handlers. I wonder if my code is designed correctly.

The issue here is that you don't reset when opening the menu.
You probably shouldn't store the timer id in state, it seems unnecessary. You also don't clear any running timeouts when the component unmounts, which can sometimes cause issues if you later enqueue state updates or other side-effects assuming the component is still mounted.
It's also considered improper to directly mutate the window.click property, you should add and remove event listeners.
You can use an useEffect hooks to handle both the clearing of the timeout and removing the window click event listener in a cleanup function when the component unmounts.
function Tile() {
const [openedMenu, setOpenedMenu] = useState(false);
const timerIdRef = useRef();
useEffect(() => {
return () => {
window.removeEventListener('click', closeMenu);
clearTimeout(timerIdRef.current);
}
}, []);
function openMenu() {
setOpenedMenu(true);
window.addEventListener('click', closeMenu);
timerIdRef.current = setTimeout(closeMenu, 3000);
}
function closeMenu() {
setOpenedMenu(false);
window.removeEventListener('click', closeMenu);
clearTimeout(timerIdRef.current);
}
function keepMenuOpened() {
clearTimeout(timerIdRef.current);
}
return(
<>
{openedMenu && (
<TileMenu
onMouseOver={keepMenuOpened}
onMouseLeave={openMenu}
/>
)}
<textarea onContextMenu={openMenu} />
</>
);
}

You need to clear previous timer when openMenu called.
function openMenu() {
// clear previous timer before open
clearTimeout(timeoutID);
// Actually open TileMenu
setOpenedMenu(true);
// Prepare TileMenu closing
window.onclick = closeMenu;
// first case: click somewhere else
setTimeoutID(setTimeout(closeMenu, 3000));
// second case: time out
console.log('open', timeoutID);
}
function closeMenu() {
setOpenedMenu(false);
window.onclick = null;
console.log('close', timeoutID);
// timer callback has executed, can remove this line
clearTimeout(timeoutID);
}

Related

is it necessary to clear timeout on button click in react?

can you please help me to tell is it necessary to clear timeout on button click in react ?
I have a example I want to show a alert on button click.here is my code
https://codesandbox.io/s/competent-torvalds-e84hq?file=/src/App.js
let id;
const onclick = () => {
// first way
id = setTimeout(() => {
alert("---");
}, 0);
// is it required ?
//clearTimeout(id);
// second way without clear
const second = setTimeout(() => {
alert("---");
}, 0);
};
useEffect(() => {
return () => {
console.log("-----", id);
// is it required ?
clearTimeout(id);
};
});
which way is better way ?
If I don't clear timeout on button click is there any performance issue ?.If there any memory leak if I don't clear timeout on button click
You only have to clear the timer if you don't want the timer callback to be called — if you want to cancel the timer callback. In fact, if you uncommented the code you've shown, the callback would be cancelled immediately and the alert would never happen.
If you don't want to cancel the callback, no, there's no need to clear the timer. The resources associated with it are released when the timer call is made; the browser automatically gets rid of its entry in the timer list.

Removing event listener can't see class function

I have an event listener to listen for Escape press, at which point my modal will close; however for some reason I am receiving an error that the close function call cannot be made.
Uncaught TypeError: this.closeModalFunc is not a function at checkEscape
My class:
checkEscape(pressed) {
if (pressed.key === 'Escape') {
this.closeModal();
}
}
componentWillMount() {
window.addEventListener('keydown', this.checkEscape);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.checkEscape);
}
closeModal () {
// code to close modal
}
I found however that if I change checkEscape into a functional const, this problem goes away and everything works as intended.
checkEscape = pressed => { /** the same innards */ };
Of course I am happy that I can get this to work, however I would like to understand why this happened...
Class components require you to do the super fun binding in the constructor, e.g.,
this.checkEscape = this.checkEscape.bind(this);
Arrow functions bind to the class auto-magically.
If that doesn't make you want to use functional components with hooks, I don't know what will. :D

Changing setTimeout not working in React with Redux

So I have a postActions.js in my code where I write all my Redux action codes.
One of the functions in it is a logout code.
This is the code.
export const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('expirationDate');
return {
type: AUTH_LOGOUT
}
}
This logout action creator is called after an expiration time has reached. This is done using a setTimeout function like below.
export const checkAuthTimeout = expirationTime => {
return dispatch => {
timeoutVar = setTimeout(() => {
dispatch(logout());
}, expirationTime*1000);
}
}
timeoutVar is declared in the top of the code, outside all the functions like this.
var timeoutVar;
Now here is my problem. Whenever a person is active in the app, I want to change setTimeout to a later time, so that he is not logged out when he is active. This is done with a code like this. logoutTime is the new time to be used for setTimeout.
clearTimeout(timeoutVar);
dispatch(checkAuthTimeout(logoutTime));
For some reason, the above code is not changing setTimeout. Lets say logoutTime is 10, it is supposed to change setTimeout to happen 10 more seconds from now, but it does not change anything.
If I only use clearTimeout(timeoutVar), the timer stops.
That is working. But when I also use dispatch(checkAuthTimeout(logoutTime)) the event happens at the old time itself. I am stuck with this for a long time. Please help.
I found the answer. Finally.
Since its React, my components were getting called multiple times and this triggered the setTimeout function multiple times. So there were multiple instances of setTimeout running. I had setTimeout called outside the function call like this -
clearTimeout(timeoutVar);
dispatch(checkAuthTimeout(logoutTime));
But checkAuthTimeout was also getting called in other locations without clearTimeout due to some lifecycle methods without clearTimeout and this created many instances of setTimeout. Safest option is to move clearTimeout inside the function. Like this.
export const checkAuthTimeout = expirationTime => {
return dispatch => {
clearTimeout(timeoutVar); // ADD THIS JUST BEFORE SETTIMEOUT
timeoutVar = setTimeout(() => {
dispatch(logout());
}, expirationTime*1000);
}
}
I just had to add a clearTimeout just before setTimeout all the time.

react - how to force an element to redraw immediately

I have a toggle button in a react component:
toggleSpeak = () => {
this.setState({buttonOn: !this.state.buttonOn});
}
And the button changes its style depending on its state:
<img key="headphones" className={audioclass} src={this.state.buttonOn ? white : black} onClick={this.toggleSpeak}/>
This also triggers some stuff in a child component:
play={this.state.buttonOn}
This triggers some speechSynthesis playback, which sometimes takes a while. The problem is that I want the user to realize that something is happening right away. The button, however, doesn't change its style right away. As long as I'm triggering something else, whether it is through a passthrough property to the child as above, or through triggering a redux action, it still delays changing color for a few seconds.
I want to change color right away without delay, so the user knows not to keep repushing it. How can I accomplish this?
this.setState({}) function is indeed asynchronous so what you are claiming is likely to be true for a very short number of milliseconds considering that all you have in the trigger is
toggleSpeak = () => {
this.setState({buttonOn: !this.state.buttonOn});
}
The noticeable delay you speak of should be unnoticeable. I would think that the delay is being imposed from elsewhere. (say you require some other synchronous code to run before this.setState({}). Do show us more of the relevant code so that we can get better grasp of what's happening.
Are you doing the speechSynthesis in render?
You should call the function that does the speechSynthesis after toggling the button.
As far as UX is concerned, I would recommend that you show a loading indicator while you are doing a task that might take some time to finish. Also, you could disable the button until the speechSynthesis is finished.
toggleSpeak = () => {
if(!this.state.doingSpeechSynthesis) {
this.setState(
{buttonOn: !this.state.buttonOn, doingSpeechSynthesis: true},
() => speechSynthesis(args, this.setState{doingSpeechSynthesis: false}));
}
}
I'm not sure if this is the "react" way of doing things, but I came up with a solution that works. I split up the property I pass to turn on the player from the button toggle.
state = {
buttonOn: false,
play: false
}
Button attributes are the same as above, changing with the buttonOn state.
ChildComponent property:
... play={this.state.play}
Then, on the button toggle event I wait a half a sec before I change the play state. This is so the button will update it's style right away, and then all the player stuff can run after a tick.
togglePlay = (newValue) => {
this.setState({play: newValue});
}
toggleSpeak = (e) => {
let newValue = !this.state.buttonOn;
this.setState({buttonOn: newValue});
if (this.state.play != newValue) {
setTimeout(function() {
this.togglePlay(newValue);
}.bind(this), 500);
}
Then of course clear the timeout function on dismount:
componentWillUnmount() {
clearTimeout(this.togglePlay);
}

Why is the "webpage is slowing down" occuring on using a while loop with a setTimeout in React?

I made this component in React with a while loop which has a setTimeout inside it. It is causing the browser to stop responding and I get "webpage is slowing down" in Firefox developers browser and in chrome too. What is the possible reason? Here is the code:-
class Adbanner extends React.Component{
changer() {
var i=0;
while(i>=0 && i<4){
setTimeout(function(){
document.getElementById('ad').src=adarray[i];
i++;
}, 2000);
}
}
render() {
return (
<div>
<input id="ad" type="image" src={ad2} style={{position:'absolute',height:'50%',width:'100%'}}></input>
{setInterval(this.changer, 2000)}
</div>
);
}
}
export default Adbanner;
How to solve it?
var i=0;
while(i>=0 && i<4){
setTimeout(function(){
document.getElementById('ad').src=adarray[i];
i++;
}, 2000);
}
The setTimeout does not wait to be complete. setTimeout sends the function to a different data structure and keeps going through the code. It just keeps cycling repeatedly as fast as computationally possible. Try doing this:
var i=0;
while(i>=0 && i<4){
setTimeout(function(){
document.getElementById('ad').src=adarray[i];
i++;
}, 2000);
console.log('created a setTimeout')
}
It looks like you're trying to run the function 4 times, while waiting for 2000ms each run. setInterval with a clearInterval condition would be perfect for your needs
function changer() {
var i=0;
let imageChange = setInterval(() => {
if (i < 4) {
document.getElementById('ad').src=adarray[i];
i++
} else {
clearInterval(imageChange)
}
}, 2000)
}
You don't want to call setInterval inside your render function. Render is supposed to be free of side-effects. What is happening is that any time the component, or its parent changes, render is getting called. That means your interval is getting created possibly many times.
Instead, move the setInterval to componentDidMount()
Additionally, in your setInterval, you are creating 4 setTimeout calls. So, every 2 seconds, you will update the ad table 4 times. That doesn't seem to be exactly what your desire is.
There are few issues in this code-
document.getElementById is not a valid way to change attributes in React.
You are executing setInterval in render method. Now Imagine what's happening- the first time render is called setInterval is executed and then four setTimeout methods are being called. Upon the execution of first timeout render is getting called again which fires a new setInterval eventually. This is causing an infinite loop of setInterval and render.
You should use a commit phase lifecycle method for setinterval method like componentDidMount so that setInterval is called only once.
The while loops until i is iterated 4 times. Now, i is iterated when setTimeout executes the function given to it, BUT that function is only executed after 2 seconds. Meanwhile, while loops a zillion times.
You're not using the power of React, though! Check out this guide on the React website that teaches you how to do this exact scenario: https://reactjs.org/docs/state-and-lifecycle.html
e.g.
class Adbanner extends React.Component {
// Set this component's state as the current ad index
state = {ad: 0};
// Rotates the ad index state every 2 seconds
rotateAd () {
this.setState({ad: (this.state.ad + 1) % adarray.length});
this.timer = setTimeout(this.rotateAd, 2000);
}
// Start rotating the ad index when mounted
componentDidMount () {
this.rotateAd();
}
// Renders when constructed, then whenever the state changes (or when React wants to)
render() {
return (
<div>
<input id="ad" type="image" src={adarray[this.state.ad]} style="position:absolute, height:50%, width:100%">
</div>
);
}
// Stop the update timer when unmounting
componentWillUnmount () {
clearTimeout(this.timer);
}
}
export default Adbanner;
I haven't tested this. Let me know how it works out.

Categories

Resources