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>
</>
)
}
Related
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
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
I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
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)
I have the following:
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // always says "false"
}, 1000);
};
Elsewhere in the code while this timer is running I'm updating the value of isPaused:
setIsPaused(true);
But this isn't reflected in the console log, it always logs false. Is there a fix to this?
The myTimer.current never changed which means isPaused is always false inside the function.
You need to make use of useEffect to update myTimer.current every time isPaused is updated.
useEffect(() => {
function startTimer() {
myTimer.current = setInterval(() => {
console.log(isPaused);
}, 1000);
};
startTimer();
return () => clearInterval(myTimer.current); // cleanup
}, [isPaused]);
You can do something like this,
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // now updates
}, 1000);
};
useEffect(() => {
startTimer();
return () => myTimer.current != null && clearInterval(myTimer.current);
}, [isPaused]);
return (
<div>
<b>isPaused: {isPaused ? "T" : "F"}</b>
<button onClick={() => setIsPaused(!isPaused)}>Toggle</button>
</div>
);
Use Others function
use useInterval from 30secondsofcode
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
Or, use react-useInterval package
function Counter() {
let [count, setCount] = useState(0);
const increaseCount = amount => {
setCount(count + amount);
};
useInterval(increaseCount, 1000, 5);
return <h1>{count}</h1>;
}