How to cancel setTimeout when navigate to another page? - javascript

Good evening, could you please advise a solution to cancel setTimeout when user navigate to other pages (for example, click other links, press back button of browser, but not change to other tabs). I have tried window event "unload", but it seems not work as expected.
My app is a normal count down, if it count to 0, it will automatically navigate to assigned page. For some purpose, I need to disable this automatic navigate if user click on other links while it is counting. Thank you so much.
import React, { useEffect } from 'react';
import {useHistory} from "react-router-dom";
const SucessPurchaseSubmit = () => {
const history = useHistory();
const navigateTo = () => history.push("/house-catalog");
useEffect(() => {
const time = document.querySelector(".time");
let count = 10;
var timer;
// automatic navigate to full catalog after 10 seconds
function countToNavigate(){
clearTimeout(timer);
time.innerHTML = count;
if (count === 0) {
navigateTo();
}
count -= 1;
timer = setTimeout(countToNavigate, 1000)
}
countToNavigate();
window.addEventListener("unload", () => {
clearTimeout(timer);
})
})
return (
<section className="success-purchase-submit">
<h1>Thank you so much for your information</h1>
<h3>One of our consultants will contact you very shortly</h3>
<h5>In the mean time, we will back to Full Catalog automatically after:</h5>
<h5 className="time">10</h5>
</section>
);
};
export default SucessPurchaseSubmit;

I may have found a solution for your problem.
const FunctionalComponent = () => {
React.useEffect(() => {
return () => {
console.log("Bye");
};
}, []);
return <h1>Bye, World</h1>;
};
This is referred from - https://www.twilio.com/blog/react-choose-functional-components
You can use clearInterval() inside the useEffect().
The useEffect() works similar to the "OG" componentWillUnmount(). This is the perfect place to clearInterval() once redirecting to new page.

According to https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect , you can return a function from useEffect to do clean up when the component is unmounted.
So, at the end of the useEffect function, you can probably add:
return () => clearTimeout(timer);
To cancel the timeout when the component is removed.

Related

I have a button that increment a counter when users click on it, but I want to delay the updating process if users click really fast

I have a button that increments a counter with useState hook when users click on it, but I want to know if there is a way to delay the state updating for 0.5 seconds when users click on the button really fast, then update the counter at once. For example, when users click on the button 1 times each second, the counter will be updated immediately. But if users click more than 3 times in one second, the state will not be updated immediately, and the counter will only be updated when users stop clicking fast. The counter will be updated to the number of clicks during the delay. I tried to use setTimeOut but it did not work. Is there a hook for this?
function App() {
// State to store count value
const [count, setCount] = useState(0);
// Function to increment count by 1
const incrementCount = () => {
// Update state with incremented value
setCount((prev)=>{
return prev+1
});
};
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
You need to apply Javascript Throttle function. Debounce is not an ideal solution here because with Debounce even after the first click user will have to wait for some time(delay) before execution happens. What you want is that on the first click counter should be incremented but after that if user clicks too fast it should not happen untill some delay ,that what Throttle function provides.
Also Thing to note that to use Throttle or Debounce in React application you will need an additional hook i.e. useCallback, which will not redfeine the function on every re-render and gives a memoized function.
More on difference between Throttle and Debounce :https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function#:~:text=Throttle%3A%20the%20original%20function%20will,function%20after%20a%20specified%20period.
Let's look at the code :
import { useState, useCallback } from "react";
function App() {
// State to store count value
const [count, setCount] = useState(0);
// Throttle Function
const throttle = (func, limit = 1000) => {
let inThrottle = null;
return (...args) => {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
// Function to increment count by 1
const incrementCount = useCallback(
throttle(() => {
// Update state with incremented value
setCount((prev) => {
return prev + 1;
});
}, 1000),
[]
);
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
export default App;
This is pure adhoc implementation. I just tried with two state variable and simple implementation. Basically,
firstly at initial click I'm doing count variable 1 instantly. Then, after each click, it will take 1 second for updating count state.
Then, I put a if block in setTimeout() method which is, if the difference between current count value and previous count value is 1, then the count variable will update. The checking is because, on each click the count variable increasing very fast. So, the condition becomes obstacle for that.
import { useState } from "react";
function App() {
// State to store count value
const [count, setCount] = useState(0);
const [prevCount, setPrevCount] = useState(0);
// Function to increment count by 1
const incrementCount = () => {
setPrevCount(count);
if(count === 0) setCount(1);
setTimeout(() => {
if(count - prevCount === 1) {
setCount(prev => prev + 1);
}
}, 1000);
};
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
export default App;

React, can't access updated value of state variable inside function passed to setInterval() in useEffect()

I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);

Running two countdown timers sequentially in React. The second one is skipped

I have created a functional component called Timer. I want to run a Timer for 3 seconds followed by a Timer for 6 seconds. However, the second timer stops as soon as it starts.
I think the problem is that the second Timer is using the state value from the first Timer. I'm not sure why it does that. I assume that the second Timer is a new instance so it shouldn't have any link with the first Timer.
Here's my code:
import React, { useEffect, useState } from 'react';
function App() {
const [timer1Up, setTimer1Up] = useState(false);
const [timer2Up, setTimer2Up] = useState(false);
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
if(timer1Up && timer2Up) {
return <>Out of all your time buddy!!</>
}
return <>I have no purpose</>;
}
interface TimerProps {
duration: number;
setTimeUp: any;
}
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeout(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
if(timeLeft === 0) {
props.setTimeUp(true);
}
});
return <>{timeLeft} s</>
}
export default App;
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
The main way react uses to tell whether it needs to mount a new component or reuse an existing one is by the component's type. The first time this renders, you return a <Timer>, so react mounts a new Timer component, which then starts doing a countdown. Once the first countdown is done you render again and you also return a <Timer>. So as far as react can tell, the only thing that changed was the props on that timer. React keeps the same component mounted, with the same state.
So there are two options 1) force it to remount, or 2) let Timer reset if its props change
To force it to remount, you will need to use keys to make it clear to react that these are different elements. That way it will unmount the old timer and mount a new one, and that new one can have its own state that it counts down
if(!timer1Up) {
return <Timer key="first" duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer key="second" duration={6} setTimeUp={setTimer2Up}/>
}
To make it work with changing props, you'll need to add logic to reset the countdown. You'll have to decide what conditions should reset the countdown, but one option is that any time the duration changes, you start over:
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeLeft(props.duration);
const intervalId = setInterval(() => {
setTimeLeft(prev => {
const next = prev -1;
if (next === 0) {
clearInterval(intervalId);
// Need to slightly delay calling props.setTimeUp, because setting
// state in a different component while in the middle of setting
// state here can cause an error
setTimeout(() => props.setTimeUp(true));
}
return next;
});
}, 1000);
return () => { clearInterval(intervalId); }
}, [props.duration]); // <---- dependency array to reset when the duration changes
return <>{timeLeft} s</>
}
For more information on how react decides to mount/unmount components, see this page on reconciliation.

set interval not updating the variable

I am trying to capture the no of clicks by the user.
Which i wish to send to an api every 15 min,
I am useing setinterval inside useEffect to achive this, but the problem is even though the state is changing outside but not inside setinterval. setinterval is only giving the initial default value.
here is the code -
const Agent = (props: RouteComponentProps) => {
const [clicks, setClicks] = useState(0);
const handleOnIdle = () => {
console.log("user is idle");
console.log("last active", new Date(getLastActiveTime()));
console.log("total idle time: ", getTotalIdleTime() / 1000);
};
const handleOnActive = () => {
console.log("user is active");
console.log("time remaining", getRemainingTime());
};
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks + 1);
};
const {
getRemainingTime,
getLastActiveTime,
getTotalIdleTime,
} = useIdleTimer({
timeout: 10000,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500,
});
useEffect(() => {
let timer = setInterval(
() => alert(`user clicked ${clicks} times`),
1000 * 30
);
return () => {
clearInterval(timer);
setClicks(0);
};
}, []);
return (
<AgentLayout>
<div className="dashboard-wrapper py-3">
<Switch>
<Redirect
exact
from={`${props.match.url}/`}
to={`${props.match.url}/home`}
/>
<Route path={`${props.match.url}/home`} component={Home} />
<Route
path={`${props.match.url}/lead-details`}
component={leadDetails}
/>
<Route
path={`${props.match.url}/fill-details`}
component={FillDetails}
/>
<Route
path={`${props.match.url}/my-desktime`}
component={MyDesktime}
/>
<Redirect to="/error" />
</Switch>
</div>
</AgentLayout>
the alert is giving user clicked 0 times
The problem is that only the first version of clicks is used by the timer function, because it closes over the version as of the first time your component function is called. When you pass an empty dependency array to useEffect, the useEffect callback is only called once, the first time the component function is called (when mounting the component).
You can fix that by having the timer function use the setClicks method instead:
useEffect(() => {
let timer = setInterval(
() => {
setClicks(currentClicks => { // ***
alert(`user clicked ${currentClicks} times`); // ***
return currentClicks; // ***
}); // ***
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the component is unmounting
};
}, []);
Now, the timer calls the setClicks using the callback form, which means it receives the current value of clicks. Because it returns that same value, it doesn't update the state of the component.
It's also possible to solve this by adding clicks as a dependency to the useEffect, but it's a bit complicated. The naive way would be just to do this:
// The naive way
useEffect(() => {
let timer = setInterval(
() => {
alert(`user clicked ${clicks} times`);
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the user's click count would get reset every time
};
}, [clicks]);
// ^^^^^^−−−−−−−−−−−−− ***
That will mostly work, but it will restart the timer every time clicks changes, even if that means that instead of waiting 30 seconds, it waits 45 (because clicks changed after 15 seconds, which cancelled the previous interval and started it again). That probably wouldn't matter for a really short interval, but for a 30 second one it seems less than ideal. Doing it this way without messing up the timing of the interval requires that you remember when the next timer callback should have happened and adjusting the duration of the initial delay to match, which gets fairly complicated.
I didn't notice earlier, but any time you're updating state based on existing state, I recommend using the callback form. So your
const handleOnAction = () => {
setClicks(clicks => clicks + 1); // Note the function callback
};
Issues
You've a stale enclosure of the clicks state, closed over from the initial render cycle when the mounting effect ran and started the interval.
Your click updater should use a functional update to update the clicks count from the previous state. This covers if the user is somehow able to click faster then the react component lifecycle and enqueue more than one clicks update in a render cycle.
T.J.'s answer is good, but I consider the useState updater function to be a pure function and the alert(`user clicked ${currentClicks} times`); in the middle of it is a side-effect and (IMHO) should be avoided.
Solution
Update handleOnAction to use a functional state update.
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks => clicks + 1);
};
I suggest using a React ref and additional useEffect to update it, to hold a cached copy of the clicks state for the interval callback to reference.
const clicksRef = useRef();
useEffect(() => {
clicksRef.current = clicks; // update clicks ref when clicks updates
}, [clicks]);
useEffect(() => {
const timer = setInterval(
() => alert(`user clicked ${clicksRef.current} times`), // use clicks ref instead
1000 * 30
);
return () => {
clearInterval(timer);
// TJ already covered removing this extra state update
};
}, []);

How to call a function every x seconds with updated state (React)

Im having a lot of trouble with this and i have tried various things. I want to call a function every second after i have clicked a start button and then have it paused after i click a stop button. I keep getting weird behaviour that i cant explain.
How can i do this in react without classes?
somethings i have treid:
const simulation = () => {
if (!running) {
console.log('hit');
return
} else {
// console.log(grid);
console.log('hey');
setTimeout(simulation, 1000)
}
}
and
enter setInterval(() => {
let newGrid = [...grid]
for (let i = 0; i < numRow; i++) {
for (let k = 0; k < numCol; k++) {
let n = 0;
}
}
console.log(grid);
}, 5000)
I have tried a lot more, In some cases it would update the state should i have added to it but not updated it after i reset the state.
How can i call a function to run every one second with updated values of state * Note the function that i want to run will update the state
You may do the following:
keep track of the current counter value along with the counter on/off state in your component state;
employ useEffect() hook to be called upon turning counter on/off or incrementing that;
within useEffect() body you may call the function, incrementing count by one (if ticking is truthy, hence timer is on) with delayed execution (using setTimeout());
once count variable is changed in the state, useEffect() gets called once again in a loop;
in order to clean up the timer upon component dismantle, you should return a callback, clearing the timer from useEffect()
const { useState, useEffect } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const App = () => {
const [ticking, setTicking] = useState(true),
[count, setCount] = useState(0)
useEffect(() => {
const timer = setTimeout(() => ticking && setCount(count+1), 1e3)
return () => clearTimeout(timer)
}, [count, ticking])
return (
<div>
<div>{count}</div>
<button onClick={() => setTicking(false)}>pause</button>
<button onClick={() => setTicking(true)}>resume</button>
</div>
)
}
render (
<App />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Late reply but maybe interesting for some people.
Look at npm package cron. In this case every 15min As easy as:
const [job] = useState(new cron.CronJob("0 */15 * * * *",async ()=>{
await updateRiderCoords();
}));
useEffect(() => {
job.start();
}, []);

Categories