I wanna stop the looping when stopState is true.
The stopState is updated when I click the stop button. but inside the startIncrement method, the stopState is always false.
This is my code:
function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(false)
function Delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function startIncrement(){
for(let i=0; i<100; i++){
console.log(stop) // always false even when i click the stop button
if(stop) i = 2000;
setNum(i)
await Delay(1000)
}
}
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p onClick={startIncrement}>{num}</p>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
Short answer:
In order to stop the previously excuted function, you will have to invoke a `clean up` function.
Detail on how it works:
In react, each render has it's own props and state, and we can explain this in action:
First, you excuted the function startIncrement(), at the time of excution, and at that perticular render phase, the value of stop is false;
Every time the num value changes, the page renders, and it just keep going... (excute below code, you can see console prints "renders!")
Then you click setStop(true) button, and at this particular render, stop === true.
However, these two steps involves two different renders, and each renders' state and everything else (props, effect...), does not affect each other, therefore, your stop value in the function never changes.
Here's an alternative to achieve the same:
export default function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(null)
console.log("renders!")
useEffect(() => {
const run = setInterval(() => {
if(stop === false && num < 100) setNum(num + 1);
}, 1000);
return () => clearInterval(run) // clean up function
}, [num, stop]);
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p>{num}</p>
<button onClick={() => setStop(false)}>start</button>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
The clean up function in useEffect can be seen as a "undo" function.
Sandbox here, you can read more about side effect from this post in Dan Abramov's blog
Related
The following code demonstrates a react functional component that has a single state variable named time. It has a button click to start which fires a function named updateTimer. This meant to move the timer from 0 to 1 to 2 to 3 and so on.
function timer() {
const [time, updateTime] = useState(0);
function updateTimer() {
setInterval(() => {
updateTime(time + 1)
},1000)
}
}
return (
<>
<span>{time} seconds</span>
<button onClick={updateTimer}>Click To Start</button>
</>
)
But what happens is that the timer stops after 1. Apparently, the value of time does not get updated. Could someone please explain this?
To update the state depending on the previous state use:
updateTime((prevTime) => prevTime + 1)
Because updateTime updates the value of time in the next render of the component, the time known to the setInterval callback is not updated and remained the initial value 0, and resets to 1 at every interval.
In a useEffect run the setter updateTime based on previous value to correct the counting, also cleanup and recreate the interval as needed when the counting starts or stops.
For a basic example, the following useEffect starts an interval when updating is set true by the button. The return runs when
updating changes or when the component unmounted for cleaning up the interval, before a new one can be recreated.
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
updating is added to the dependencies array so that useEffect listens to the changes of this state, and creates a new interval if it is true.
The setter updateTime does not change and it sets time based on the previous value internally, so updateTime and time do not need to be included in the dependencies array.
Example in full snippet:
const { useState, useEffect, Fragment } = React;
function Timer() {
const [time, updateTime] = useState(0);
const [updating, setUpdating] = useState(false);
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
return (
<Fragment>
<p>{time} seconds</p>
<button onClick={() => setUpdating((prev) => !prev)}>{`Click To ${
updating ? "Stop" : "Start"
}`}</button>
</Fragment>
);
}
const App = () => {
return (
<div className="App">
<Timer />
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
.App {
font-size: x-large;
}
button {
padding: 9px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
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;
function Test() {
// const [value, setValue] = useState('')
// const [display, setDisplay] = useState(false)
let value = '';
let display = false;
/* Debounce function */
const handleChange = (e) => {
// setValue(e.target.value)
let timer;
if (timer) {
clearTimeout(timer)
}
if (value.length === 1 && e.target.value.length === 0) {
// setDisplay(false)
}
else {
timer = setTimeout(() => {
// setDisplay(true)
// dispatch(loadSearchResult(e.target.value, fiatCurrencyUuid))
console.log(e.target && e.target.value);
}, 500)
}
}
return (
<div>
<input type='text' placeholder='Search' value={value} onChange={e => handleChange(e)} />
{
display
? result
.map(item => {
return (
<li key={item.id}>{item.id}</li>
);
})
: undefined
}
</div>
);
}
// export default Test
ReactDOM.render(<Test/>, document.querySelector("#test"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="test"></div>
I have a debounce function which executes 500ms after the user stopped typing in the input. The debounce function set the display to true when something is typed and to false if there is only one letter present in the input so as to close the div modal. It is working fine if the input is changing one letter by letter but if it has for example "hello" text and I clear the input very fastly without letting the timeout to run again, then the display is first set to false and the div modal closes but suddenly it automatically becomes true and the div modal again renders.
The issue has to do on how react works (which is complicated), every time you call the setValue function and runs the rest of the debounce logic, this dispatches a command to update the value sometime in the future (very small time, almost instantly, but it is still asynchronous and not in the current execution loop). When the value gets updated, react will call the component function again to show the updated value on the screen, also creating a new handleChange function, with a new timer variable that has nothing todo with the previous one, for this reason, every change you make on the input, creates a new timer without cancelling the previous, and eventually all of them will execute in sequence.
To fix this you can hold the timer instance in a react ref, this way, all the handleChange functions created over time will still use the same variable reference and it will work properly:
const [value, setValue] = useState('')
const [display, setDisplay] = useState(false)
const timer = useRef()
/* Debounce function */
const handleChange = (e) => {
setValue(e.target.value)
if (timer.current) {
clearTimeout(timer.current)
}
if (value.length === 1 && e.target.value.length === 0) {
setDisplay(false)
}
else {
timer.current = setTimeout(() => {
setDisplay(true)
dispatch(loadSearchResult(e.target.value, fiatCurrencyUuid))
}, 500)
}
}
return (
<>
<input type='text' placeholder='Search' value={value} onChange={e => handleChange(e)} />
{display ?
result.map(item => {
return <li key={item.id} /> : undefined
}
</>
)
Run button has already been clicked.
When I click on the skip button I am unable to reach Switch Case 2. The component state skip gets updated but the function still prints the old value of skip.
const Component = () => {
const [skip, setskip] = useState(false);
const [runstate, setrunstate] = useState(1);
const run = async () => {
switch(runstate) {
case 1: {
if(skip) {
setrunstate(2);
}
else {
console.log(skip , "Stuck in Step 1") // false, Stuck in Step 1 even after clicking skip
setTimeout(run, 250)
}
break;
}
case 2: {
console.log("Reached Step 2")
}
}
}
return (
<>
<button onClick={run}> Run </button>
<button onClick={() => setskip(true)}> Skip </button>
</>
)
}
Can anybody tell what might be causing this or a correct way to achieve this?
Why recursion is not working?
There are a lot of factors behind this. So, let's understand with a minimal reproducible example of your use case and breaking the code step-by-step.
const [foo, setFoo] = useState(0);
const recursiveCallback = useCallback(() => {
setFoo(foo + 1);
// an infinite recursion occurs as
// termination condition never satisfies
if (foo <= 2) {
// current reference is being called
// with the current value of foo in
// the scope, i.e. lexical environment
recursiveCallback();
}
}, [foo]);
return (
<>
<p>{foo}</p>
<button onClick={recursiveCallback}>Recursive Callback</button>
</>
);
When recursiveCallback() is called for the very first time, a closure is created with the foo's value being 0.
When foo is incremented with setFoo(foo + 1), a new reference of recursiveCallback is created in the memory (let's say x) which would have the updated value of foo in it.
Now, when recursiveCallback() is called again, it does not call the x reference of itself but the current reference from where it's being called from, which would have the value of foo present in the lexical environment, i.e. 0. So, it appears that foo is not incrementing but in actual it is, as you can see in the <p>{foo}</p>.
So, the solution to fix the above snippet would be to call the function in a useEffect which would always call a new reference of nonRecursiveCallback every time the value of foo is updated!
// note that the function is no more recursive
const nonRecursiveCallback = useCallback(() => {
// updated foo is logged
console.log(foo);
// incrementing foo
setFoo(foo + 1);
}, [foo]);
useEffect(() => {
if (foo && foo <= 2) {
// new reference is being called
// with an updated value of foo
nonRecursiveCallback();
}
}, [foo, nonRecursiveCallback]);
Solution
Instead of calling run() recursively, you can have it called repetitively with the help of useEffect hook unless the termination condition is not satisfied.
const { useState, useEffect, useCallback } = React;
const Component = () => {
const [skip, setSkip] = useState(false);
const [runState, setRunState] = useState(1);
// two additional local states have been introduced
// which would help trigger useEffect repetitively
// after user has clicked on run button
const [runEffect, setRunEffect] = useState(false);
const [toggleEffect, setToggleEffect] = useState(false);
useEffect(() => {
if (runEffect) {
switch (runState) {
case 1: {
if (skip) {
setRunState(2);
} else {
console.log(skip, 'Stuck in Step 1');
// toggle a boolean value which
// would trigger this hook again
setTimeout(() => setToggleEffect(!toggleEffect), 250);
}
break;
}
case 2: {
console.log('Reached Step 2');
setRunEffect(false);
break;
}
default:
break;
}
}
}, [runEffect, toggleEffect, runState, skip]);
const startRecursion = useCallback(() => {
setRunEffect(true);
setToggleEffect(true);
}, []);
const handleSkip = useCallback(() => {
setSkip(true);
}, []);
return (
<React.Fragment>
<button onClick={startRecursion}>Run</button>
<button onClick={handleSkip}>Skip</button>
</ React.Fragment>
);
};
// Render it
ReactDOM.render(
<Component />,
document.getElementById("react")
);
<div id="react"></div>
<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>
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();
}, []);