Changing setTimeout not working in React with Redux - javascript

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.

Related

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

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

Angular 6 setTimeout and clearTimeout error

I have an angular 6 strange problem.
I am using setTimeout and clearTimeout functions to start/cancel the timeout.
However this sometimes works, and sometimes doesn't.
Even if the user triggers an (click) event and the clearTimeout is run, sometimes it forces player to draw two cards.
Here is the code
//an event that says we must call uno
this._hubService.mustCallUno.subscribe(() => {
this.mustCallUno = true;
this._interval = window.setInterval(() => {
this.countdown -= 100;
}, 100);
this._timer = window.setTimeout(() => {
if (this.mustCallUno) {
this.drawCard(2);
this.callUno();
}
}, 2000);
});
// a function player calls from UI to call uno and not draw 2 cards
callUno() {
this.mustCallUno = false;
window.clearTimeout(this._timer);
window.clearInterval(this._interval);
this.countdown = 2000;
}
So even if the player calls callUno() function, the setTimeout is executed. Even worse, the code goes through the first if check inside the setTimeout if( this.mustCallUno) which by all means should be false since we just set it to false when we called callUno() function this.mustCallUno = false;.
I used setTimeout (returns NodeJS.Timer) before window.setTimeout and the result was the same.
You're using angular6+, so I suggest you to use reactive programming library such as rxjs
I made you a small example here.
Check for the possibility where function in this._hubService.mustCallUno.subscribe is run twice or multiple times, usually initially which you might not be expecting. Put a logger in function passed to mustCallUno.subscribe and callUno.
In this case what might be happening is this._timer and this._interval will have a different reference while the old references they hold, were not cleared because callUno is not called or is called less number of times than the callback in subscribe.

React Native background timer never stops

I'm building an app that has a timer to request geolocation while the timer is active. For a timer I'm using react-native-background-timer. Which is kind of working, but not exactly as I want.
With this piece of code:
BackgroundTimer.runBackgroundTimer(() => {
console.log('tic');
},
1000);
the timer is stopping when the app is in the background. While with this piece of code:
const intervalId = BackgroundTimer.setInterval(() => {
console.log('tic');
}, 1000);
it runs constantly even in the background, but I'm not able to stop it. When I run BackgroundTimer.clearInterval(intervalId);, the timer still runs. Even when I leave the screen and go back to my Home screen, the timer still ticks and never stops. This is not ideal, because I need timer to run for a few minutes and then stop.
I set the timer to 1 second to update time left on the screen. I was thinking about setting a timer once for 6 minutes, but then how do I update the state every second? Making 2 timers for this feels like a bad practise, even though it would work.
So to make it more clear, the user suppose to engage in certain activity like walking for a few minutes. So I can't let the timer to stop when user is answering a call or opened a music app to switch music or something else. Timer still needs to run and I need to measure the number of steps and distance with geolocation. It need to work flawlessly even if user opened another app, forgot about my app, and it would still run for the remaining time, then made a record to the database and stopped.
Try the following code snippet,
works both on android and ios
import { DeviceEventEmitter, NativeAppEventEmitter, Platform } from 'react-native';
import _BackgroundTimer from 'react-native-background-timer';
const EventEmitter = Platform.select({
ios: () => NativeAppEventEmitter,
android: () => DeviceEventEmitter,
})();
class BackgroundTimer {
static setInterval(callback, delay) {
_BackgroundTimer.start();
this.backgroundListener = EventEmitter.addListener("backgroundTimer", () => {
this.backgroundTimer = _BackgroundTimer.setInterval(callback, delay);
});
return this.backgroundListener;
}
static clearInterval(timer) {
if (timer) timer.remove();
if (this.backgroundTimer)
_BackgroundTimer.clearInterval(this.backgroundTimer);
_BackgroundTimer.stop();
}
}
export default BackgroundTimer;
Usage
const timer = BackgroundTimer.setInterval(callback, 1000);
BackgroundTimer.clearInterval(timer)
For some reason I got the following error when using #Aravind Vemula's answer.
When calling BackgroundTimer.clearInterval(timer); the following error appears:
timer.remove is not a function
That's why I modified the code slightly.
// parameter removed
static clearInterval() {
// ADD this if statement
if (this.backgroundListener){
this.backgroundListener.remove();
}
if (this.backgroundTimer)
_BackgroundTimer.clearInterval(this.backgroundTimer);
_BackgroundTimer.stop();
}
The above code checks if a backgroundlistener is registered. If yes it removes all listeners and especially our backgroundTimer.
Usage:
BackgroundTimer.clearInterval(); // removed parameter
After my change, everything is working fine on iOS 14.3.
#Aravind Vemula's answer working properly. But if user open the app from the background and timer code is added in background handler then when you stop the timer it is not working. So following changes you need to make in both the methods.
static setInterval(callback, delay) {
if (!this.backgroundListener && !this.locationTimer) {
_BackgroundTimer.start();
this.backgroundListener = EventEmitter.addListener('backgroundTimer', () => {
this.locationTimer = _BackgroundTimer.setInterval(callback, delay);
});
return this.locationTimer;
}
}
static clearInterval() {
if (this.backgroundListener) {
this.backgroundListener.remove();
}
if (this.locationTimer) {
_BackgroundTimer.clearInterval(this.locationTimer);
}
this.backgroundListener = false;
this.locationTimer = false;
_BackgroundTimer.stop();
_BackgroundTimer.start();
}

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