Following is the code which increments the value at once after 4 sec, though I am expecting the batch of update should increment the valus after 4 sec only on multiple clicks.
Ex. - Let us say, I clicked the "Async Increase" button 5 times, then after 4 sec the counter increases to 1,2,3,4,5 but I want after 4 sec it should increment making it 1 then after 4 sec it should increment it to 2, then after 4 sec it should increase to 3 and so on.
Let me know how can I fix this.
Code -
const UseStateCounter = () => {
const [value, setValue] = useState(0);
const reset = () => {
setValue(0);
}
const asyncIncrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue + 1);
}, 4000);
}
const asyncDecrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue - 1);
}, 4000);
}
return <>
<section style={{margin: '4rem 0'}}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</>
};
export default UseStateCounter;
To do that, wait for the previous change to finish before you start the next one. For instance, one way to do that is with a promise chain; see comments:
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
export default UseStateCounter;
Live Example:
const {useState, useRef} = React;
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
ReactDOM.render(<UseStateCounter />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Note: Normally I make a big noise about handling promise rejections, but none of the promise stuff above will ever reject, so I'm comfortable not bothering with catch in queueValueUpdate.
Related
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'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>
</>
)
}
Why my sleep function doesn't make the React application freeze? Here's my code:
import React from "react";
import "./App.css";
function App() {
const [count, setCount] = React.useState(0);
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
export default App;
So, I have an IIFE sleep function inside the component that is supposed to execute before every render. But when I click on the increment button of my counter, the DOM being updated immediately without waiting for my sleep function to finish its execution. What's wrong with it? If I use for loop to freeze the app everything works as expected but the sleep function implemented with promise doesn't cause my app freeze.
What this block of code does:
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
is it creates a Promise that resolves after 5 seconds. That's it. The Promise isn't used anywhere, and so it isn't connected to anything in the rest of the code.
function App() {
const [count, setCount] = React.useState(0);
// here, create a Promise that resolves after 5 seconds, and don't do anything with it
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
The App's return still executes immediately when App is called, so there's no delay before it renders.
If you wanted to add a render delay, conditionally render the component and set a state after 5 seconds.
function App() {
const [count, setCount] = React.useState(0);
const [render, setRender] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setRender(true);
}, 5000);
}, []);
return !render ? null : (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</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>
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)