useEffect is trigged unexpectedly - javascript

The useEffect hook is getting called unexpectedly. Once the timer reaches zero and when I set it to 10 (using the resend Otp button), then the timer starts to decrease faster than 1 second.
My component is as follows:
const EnterOtpView = ({ touched, errors, isSubmitting }) => {
const [timer, setTimer] = useState(3);
useEffect(() => {
if (timer > 0) {
setInterval(() => setTimer((prevTimer) => prevTimer - 1), 1000);
}
}, [timer]);
return (
<div>
{timer > 0 ? (
<p>Resend Otp in: {timer} seconds</p>
) : (
<button
type='button'
className='btn btn-link'
onClick={() => setTimer(10)}
>
resend otp
</button>
)}
</div>
);
};
What I have tried:
removed the timer from the dependencies array of useEffect
create a separate function to reduce timer
Any help will be appreciated. 😇

Here is a working example of how you could set an interval once, prevent it from going negative, and clear it on unmount:
const {useState, useEffect} = React;
const Example = () => {
const [timer, setTimer] = useState(3);
useEffect(() => {
// setInterval returns the id of the interval so we can clear it later
const id = setInterval(() => {
setTimer((prevTimer) => {
// Move our negative logic inside the functional update
if (prevTimer > 0) {
return prevTimer - 1;
}
return 0;
})}, 1000);
// Clear the interval on umount
return () => {
clearInterval(id);
}
}, []);
return (
<div>
{timer > 0 ? (
<p>Resend Otp in: {timer} seconds</p>
) : (
<button
type='button'
className='btn btn-link'
onClick={() => setTimer(10)}
>
resend otp
</button>
)}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

The solution that worked for me:
useEffect(() => {
setInterval(() => setTimer((prevTimer) => prevTimer - 1), 1000);
}, []);
I just changed the useEffect as above, i.e. made it into the componentDidMount form of useEffect and now its working fine.

Remove "timer" dependency from useEffect..
It will call everytime this function whenever timee

Related

Callback Function of setInterval is not having access to updated state if the state is updated after the interval is started

export default function App() {
const [active, setActive] = useState("");
function handleButtonClick(value) {
if (!active) {
setActive(value);
setTimeout(() => {
setActive("");
console.log(active);
}, 3000);
} else {
let interval = setInterval(() => {
console.log(active);
if (!active) {
setActive(value);
setTimeout(() => {
setActive("");
}, 3000);
clearInterval(interval);
}
}, 0);
}
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "space-between"
}}
>
<div>
<button onClick={() => handleButtonClick("first")}>first</button>
<button onClick={() => handleButtonClick("second")}>second</button>
<button onClick={() => handleButtonClick("third")}>third</button>
</div>
<div>{active.length > 0 && <p>{active} button Clicked</p>}</div>
</div>
);
}
The callback function of setInterval in else case of handleButtonClick function is not having access to updated state, if the setInterval is started and then after that state is updated then the callback function is only having access to previous state but not updated state. How can i fix this
Required functionality
when any button is clicked the active state is set to respective value and then after 3 seconds with setTimeout the state is changed to empty string as i want it to act it as a poppup.
if another button is clicked before 3 seconds then it should wait for current 3 seocnds to complete and should show the respestive button clicked popup
The active identifier that the interval callback closes over will always be the same inside the interval - it'll be that one const [active ... that was declared at the rendering where the handleButtonClick was created.
It looks like you want to queue up button values pressed when one is already active - consider pushing the future values to a stateful array, and use useEffect to move the first value in that array to the active state when needed. This way, you'll never be depending on values in a possibly stale closure.
const { useState, useEffect } = React;
function App() {
const [active, setActive] = useState("");
const [queue, setQueue] = useState([]);
const toggleActive = (val) => {
setActive(val);
const timeoutId = setTimeout(() => {
setActive('');
}, 3000);
// don't forget to clean up
return () => clearTimeout(timeoutId);
};
function handleButtonClick(value) {
if (active) {
setQueue([...queue, value]);
} else {
toggleActive(value);
}
}
useEffect(() => {
// Run the next value in the queue, if active is empty and the queue is not
if (!active && queue.length) {
toggleActive(queue[0]);
setQueue(queue.slice(1));
}
}, [active]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "space-between"
}}
>
<div>
<button onClick={() => handleButtonClick("first")}>first</button>
<button onClick={() => handleButtonClick("second")}>second</button>
<button onClick={() => handleButtonClick("third")}>third</button>
</div>
<div>{active.length > 0 && <p>{active} button Clicked</p>}</div>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
The required functionality is probably called 'debounce' and can be easily implemented in React using the 'useEffect' hook:
const [active, setActive] = useState('')
useEffect(() => {
const timer = setTimeout(() => setActive(''), 3000);
return () => clearTimeout(timer);
}, [active]);
const handleClick = (value) => {
setActive(value);
}
Here's an alternative approach that uses promises instead of timeout. I think you will find that the complexity is reduced. Note we are careful to avoid setState calls on unmounted components. See the You Might Not Need An Effect guide from React -
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
function App() {
const [active, setActive] = React.useState("")
const queue = React.useRef(Promise.resolve())
const mounted = React.useRef(true)
React.useEffect(_ => _ => { mounted.current = false },[])
const alert = message => event => {
queue.current = queue.current
.then(_ => mounted.current && setActive(message))
.then(_ => sleep(3000))
.then(_ => mounted.current && setActive(""))
}
return (
<div>
<button onClick={alert("first")}>first</button>
<button onClick={alert("second")}>second</button>
<button onClick={alert("third")}>third</button>
{active && <p>{active}</p>}
</div>
)
}
ReactDOM.createRoot(document.querySelector("#app")).render(<App />)
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
setState (here setActive) is a async function hence updated value wont be print in console used just after it.
you can use
setActive(prevState => { return updatedState; }
here prevState will have access to updated state

Why is prior useEffect() still running for a significant amount of time even after the component is rendered?

I'm implementing stopwatch in ReactJs this is how my code looks as of now:
const App: React.FC = () => {
const [seconds, setSeconds] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const secondsToTimerFormat = (seconds: number): string => {
console.log(seconds)
return (seconds-seconds%60)/60+":"+seconds%60
}
const manipulateTimer = (toPauseTimer: boolean) => {
setIsPaused(toPauseTimer);
}
useEffect(() => {
if(!isPaused){
setTimeout(() => {
setSeconds(seconds + 1)
}, 1000)
}
}, [seconds, isPaused])
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button onClick={() => {manipulateTimer(true)}}>Pause</button>
<button onClick={() => {manipulateTimer(false)}}>Resume</button>
<button onClick={() => {
setSeconds(0);
}}>Reset</button>
</div>
</div>
);
}
I'm expecting this to work normally. But the "Reset" button is not working as expected.
If I click on "Reset" after 13 seconds, this is the console.log() output.
If I add a new variable inside useEffect(), say something like let execute: boolean = true and then set it to false in useEffect() clean up, everything is working as expected.
So, I know the fix, but I want to know the reason behind the current behaviour. I understand that when I click on reset, there is already a useEffect() running with seconds value as 13. But since its setTimeout() ends in one second and at the same time, I'm doing setSeconds(0), why would the previous useEffect() run multiple times before coming to halt?
Issues like this usually arise because the timers being used are not being cleared between renders. Also, when the next state depends on the current state, it is better to use the second form of the state setter function which takes the current state as the parameter and returns the next state. Modify the useEffect as given below to get this to work:
useEffect(() => {
let timer;
if (!isPaused) {
timer = setTimeout(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [seconds, isPaused]);
Try using setInterval and separate methods for handling the timer state:
import { useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [intervalId, setIntervalId] = useState(0);
const secondsToTimerFormat = (seconds) => {
console.log(seconds);
return (seconds - (seconds % 60)) / 60 + ":" + (seconds % 60);
};
const handleStart = () => {
const id = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
setIntervalId(id);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleReset = () => {
handlePause();
setSeconds(0);
};
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button
onClick={() => {
handlePause();
}}
>
Pause
</button>
<button
onClick={() => {
handleStart();
}}
>
Resume
</button>
<button
onClick={() => {
handleReset();
}}
>
Reset
</button>
</div>
</div>
);
}
Link to sandbox

How can I start / stop setInterval?

I've tried different ways, but It doesn't works.
[...]
const [automatic, setAutomatic] = useState(false);
[...]
var startAuto;
useEffect(() => {
if (!automatic) {
console.log("stop");
clearInterval(startAuto);
} else {
startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
}
}, [automatic]);
[...]
<Button
onPress={() => setAutomatic(!automatic)}
title="turn on/off"
/>
[...]
It works when I put a setTimeout outside the useEffect, that way:
setTimeout(() => { clearInterval(startAuto); alert('stop'); }, 10000);
But I want to have a button to start / stop
Your var startAuto; is redeclared on each render, and since changing the state causes a re-render, it never holds the reference to the interval, which is never cleared.
Use the useEffect cleanup function to clear the interval. Whenever automatic changes, it would call the cleanup (if returned by the previous invocation), and if automatic is true it would create a new interval loop, and return a new cleanup function of the current interval.
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
Working example:
const { useState, useEffect } = React;
const Demo = () => {
const [automatic, setAutomatic] = useState(false);
const [question, changeQuestion] = useState(0);
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion(q => q + 1);
}, 5 * 100);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
return (
<div>
<button
onClick={() => setAutomatic(!automatic)}
>
turn {automatic ? 'off' : 'on'}
</button>
<p>{question}</p>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
For example, you can check and use this hook:
https://usehooks-ts.com/react-hook/use-interval
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}

Latest function callback is not caught in React

In the example bellow, Child component calls onFinish callback 5 seconds after clicking on button. The problem is that onFinish callback can change in those 5 seconds, but the it will call the last caught one.
import React, { useState } from "react";
const Child = ({ onFinish }) => {
const [finished, setFinished] = useState(false);
const finish = async () => {
setFinished(true);
setTimeout(() => onFinish(), 5000);
};
return finished ? (
<p>Wait 5 seconds and increment while waiting.</p>
) : (
<button onClick={finish}>Click here to finish</button>
);
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onFinish={() => alert(`Finished on count: ${count}`)} />
</>
);
}
The workaroud for this one is to replace finish with the following:
const cb = useRef();
cb.current = onFinish;
const finish = async () => {
setFinished(true);
setTimeout(() => cb.current(), 5000);
};
Is there a better approach to update the callback to the latest one?
Yes, you can check the current state and compare it with prev state like
setFinished((prevState) => newState)

Reset of 1 minute counter in hooks isn't working properly

I'm creating 1 minute counter and it's working but when I click on the reset button to restart the same counter, getting weird behavior.
Here is the code
export default function App() {
const [counter, setCounter] = useState(60);
useEffect(() => {
counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
}, [counter]);
const handleReset = () => {
setCounter(60);
}
return (
<div className="App">
<h1>Counter</h1>
<p>{counter}</p>
<button onClick={handleReset}>Reset</button>
</div>
);
}
Code in Sandbox
Can somebody help me to improve it? thanks
Consider having useEffect with an empty dependency array and an interval instead of a timeout. Then, in the timeout callback, decrement counter if it's above 0:
const App = () => {
const [counter, setCounter] = React.useState(60);
React.useEffect(() => {
setInterval(() => {
// Must use callback form here; outer `counter` is in stale closure
setCounter(counter => counter === 0 ? counter : counter - 1);
}, 1000);
}, []);
const handleReset = () => {
setCounter(60);
}
return (
<div className="App">
<h1>Counter</h1>
<p>{counter}</p>
<button onClick={handleReset}>Reset</button>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class="react"></div>
Your current implementation with the [counter] dependency array means that a new recursive timeout is being set every time counter changes, which is not desirable; you'll get multiple recursive timeouts running at the same time.
The problem, if I understand correctly, is at every update a new setTimeout gets created and all created timeouts simultaneously update the counter. Instead of using setTimeout, try using setInterval, which you can clean up by the end of the hooks life.
useEffect(() => {
let interval = setInterval(
() => setCounter(counter > 0 ? counter - 1 : counter),
1000
);
return () => clearInterval(interval);
}, [counter]);

Categories