Make function fire only once after spamming button in React - javascript

I'm kinda piggybacking off a question I asked earlier. But in the following piece of code, I want to be able to click this button multiple times but then fire the function only once. I tried using a setTimeout but the function will still fire multiple times after the delay. Next, I've tried debounce from lodash, but this is just behaving exactly the same what as the setTimeout Have I missed something in my code?
const handler = debounce(() => {
myFunction(name, age)
}, 1000);
function update() {
handler();
}
<StyledQuantityButton disabled={ amount <= 0 }
onClick={() => {
const newValue = amount - 1;
setAmount(newValue);
update();
}} >
—
</StyledQuantityButton>

While you might have put the myfunction(name, age) part in debounce, your setAmount part is still outside the debounce.
Depending on how your component re-renders, the debounced function might also get recreated (therefore losing its "I just got called" status), which would lead to the same problem. Try this:
const handler = React.useMemo(() => debounce(() => {
setAmount(amount => amount - 1);
myFunction(name, age);
}), [myFunction]);
// ...
onClick={handler}

Related

Debounce same function with two different events

I want to debounce same function in two diferent scenarios, when user clicks search button and when user stops typing. So if the user types cat and in less than 1 second he clicks 3 times search icon we will only search one time, after 1 second he stops clicking search button.
I tried this:
function debounce(your_func,time=1000){...}
function search_api(){...}
$("#my_input").on("input",
debounce(function(){search_api()})
);
$("#search_button").on("click",
debounce(function(){search_api()})
);
This works but not exacly what we want cause it debouce it "separately", so it debounces inputs by on hand and clicks to search on the other hand.
This is because you trigger the same function so it will work as you expected.
You might think as the debounce function have a private var: timer(returned by setTimeout).
Each time you trigger this function it reset the old timer, and send a new SetTimeout, here is a simple version of debounce function.
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function saveInput(){
console.log('Saving data');
}
const processChange = debounce(() => saveInput());
If you separate two function , which means you create two new debounced function , and they both have a private timer and setTimeout function
I found the solution but don't know why is working despite reading a lot of documentation about functions and callbacks:
function debounce(your_func,time=1000){...}
function search_api(){...}
const debounce_search = debbounce(search_api)
$("#my_input").on("input",
debounce_search
);
$("#search_button").on("click",
debounce_search
);

React setInterval clearInterval duplicating State

I'm attempting to make a basic count-down timer in React. It should start at 30 seconds, and count down by 1 each second. When clicking the button, the timer should restart at 30 and begin the countdown again.
This seems simple enough, and it's printing to the console exactly as I expect it to. However, as soon as I try to update state with my timer so I can render the countdown on-screen (uncomment the commented line below) the console.log duplicates, the render doubles, and I seem to have two states running simultaneously. I'm not sure what to do about this.
Any help is greatly appreciated!
const [seconds, setSeconds] = useState(30)
let interval = null
function startTimer() {
stopTimer()
let start = 30
interval = setInterval(() => {
// setSeconds(start)
console.log('start: ', start)
start--
}, 1000)
}
function stopTimer() {
clearInterval(interval)
}
return (
<p>{seconds}s</p>
<button onClick={startTimer}>Start</button>
)
I've looked around to see what I could find myself before posting. I read through a number of articles on React and setInterval, and watched some tutorials, but couldn't find what I was looking for. I attempted to rewrite the code in different ways but always ended with the same result.
There are multiple things to say, like why use async/await when there is nothing to await for, why use a local variable start = 30 when you just want to decrease your seconds count and why you declare the interval in the function body. A React functional component will run all its code and in your case do let interval = null everytime it rerenders. You have to store the interval somewhere else, like here as a global variable. Moreover, when you create the setInterval, it won't have access to the new seconds count. What you can do is use the arrow function form inside your setState function. Doing so, you will get the right seconds variable.
Maybe the code below will help you find out what's wrong:
let interval = null
function App(props) {
const [seconds, setSeconds] = React.useState(30)
function startTimer() {
stopTimer()
interval = setInterval(() => {
setSeconds((seconds) => seconds - 1)
}, 1000)
}
function stopTimer() {
clearInterval(interval)
setSeconds(30)
}
return (<button onClick={startTimer}>{seconds}</button>)
}
Thanks for the help folks!
Turns out, regardless of how I assign the state to seconds (arrow function or a separate variable) the cause of the issue here was the placement of the 'interval' variable.
Thomas's answer was correct. Moving it outside of the functional component rather than inside corrected the issue. If the variable was within the function the interval seemed like it didn't fully clear, just paused, and then there were two intervals running simultaneously.
Here's the final code, functioning as expected.
import { useState } from "react"
let interval = null
export default function app() {
const [seconds, setSeconds] = useState(30)
function startTimer() {
stopTimer()
interval = setInterval(() => {
setSeconds((seconds) => seconds - 1)
}, 1000)
}
function stopTimer() {
clearInterval(interval)
setSeconds(30)
}
return (
<p>{seconds}s</p>
<button onClick={startTimer}>Start</button>
</div>
)
}

Set Interval only running function once and clear interval is not working (Using react and redux)

I am trying to run a function continuously until a condition is met, but in this test case, until the off button is pressed. My first issue is that the function does not stop when i press the off button.
let intervalId
function on(){
intervalId = window.setInterval(function(){
setnum(num=>num+1)
//setnum(num + 1)
//Line 11 results in the number going up once and if i keep pressing the button it goes up by one but flashes between numbers more and more frantically every press. The off button has no effect.
//updateUserMoney()
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
{num}
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
The second issue is that the function I want to run in the interval (that setnum is standing in for) is actually
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
Here, the incrementTime function works as intended and continues to increment, but the update user function only fires once.
I think it has the same problem that line 11 has where setnum(num + 1) doesn't work but setnum(num => num + 1) does. I haven't used the second syntax much and don't understand why it's different can anybody tell me?
Here's the full code
import { useState } from "react";
import { batch, useDispatch, useSelector } from "react-redux";
import { incrementTime, updateUser } from "../actions";
const GeneralActions = () => {
const dispatch = useDispatch()
const user = useSelector((state)=>state.user)
const [num, setnum]= useState(0)
let intervalId
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
function on(){
intervalId = window.setInterval(function(){
updateuserMoney()
setnum(num=>num+1)
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
<br/>
<>{num}</>
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
);
}
export default GeneralActions;
Any insight is appreciated. Thank you!
Every time you set a new state value in React, your component will rerender. When your GeneralActions component rerenders, your entire function runs again:
const GeneralActions = () => {
// code in here runs each render
}
This means things such as intervalId, will be set to undefined when it runs let intervalId; again, and so on this particular render you lose the reference for whatever you may have set it to in the previous render. As a result, when you call off(), it won't be able to refer to the intervalId that you set in your previous render, and so the interval won't be cleared. If you want persistent variables (that aren't related to state), you can use the useRef() hook like so:
const GeneralActions = () => {
const intervalIdRef = useRef();
...
function on(){
clearInterval(intervalIdRef.current); // clear any currently running intervals
intervalIdRef.current = setInterval(function(){
...
}, 400);
}
function off(){
clearInterval(intervalIdRef.current);
}
}
One thing that I've added above is to clear any already created intervals when on() is executed, that way you won't queue multiple. You should also call off() when your component unmounts so that you don't try and update your state when the component no longer exists. This can be done using useEffect() with an empty dependency array:
useEffect(() => {
return () => off();
}, []);
That should sort out the issue relating to being unable to clear your timer.
Your other issue is with regards to setNum() is that you have a closure over the num variable for the setTimeout() callback. As mentioned above, every time your component re-renders, your function body is executed, so all of the variables/functions are declared again, in essence creating different "versions" of the num state each render. The problem you're facing is that when you call setInterval(function(){}), the num variable within function() {} will refer to the num version at the time the function was created/when setInterval() function was called. This means that as you update the num state, your component re-renders, and creates a new num version with the updated value, but the function that you've passed to setInterval() still refers to the old num. So setNum(num + 1) will always add 1 to the old number value. However, when you use useState(num => num + 1), the num variable that you're referring to isn't the num "version"/variable from the surrounding scope of the function you defined, but rather, it is the most up to date version of the num state, which allows you to update num correctly.

REACT - Updating state and then doing a console.log , shows unupdated state

After clicking the button the console shows 0 and the page 1
function App() {
const [count, setCount] = useState(0);
const addOne = () => {
setCount(count + 1)
console.log(count)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
I think is because the setCount() is happening asynchronously but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state
Why???
The state updation in React is always asynchronous. you will find the updated state value of count in useEffect
function App() {
const [count, setCount] = useState(0);
useEffect(()=> {
console.log('count',count);
},[count])
const addOne = () => {
setCount(count + 1)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
Closures
You are experiencing the unupdated state in the console log, because of closures.
when your function is created when the component is rendered, and closure is created with the value of count at the time the closure is created.
if the value of count is 0, and your component rerenders, a closure of your function will be created and attached to the event listener of the onlcick.
in that case, the first render of your component
const addOne = () => {
setCount(count + 1)
console.log(count)
}
is equivalent to (replace count with 0)
const addOne = () => {
setCount(0 + 1)
console.log(0)
}
therefore it makes sense in your case that count is 0 when it is console logged.
In this case, I believe its the closure you are experiencing combined with the asynchronous behavior of setState
Async behaviour
codesandbox
Async behaviour becomes a problem when asynchronous actions are occuring. setTimeout is one of the basic async actions. Async actions always require that you provide a function to the setCount function, which will accept the latest state as a parameter, with the nextState being the return value of this function. This will always ensure the current state is used to calculate the next state, regardless of when it is executed asynchronously.
const addOneAsync = () => {
setCountAsync((currentState) => {
const nextState = currentState + 1;
console.log(`nextState async ${nextState}`);
return nextState;
});
};
I have created a codesandbox demonstrating the importance of this. CLick the "Count" button fast 4 times. (or any number of times) and watch how the count result is incorrect, where the countAsync result is correct.
addOneAsync:
when the button is clicked, a closure is created around addOneAsync, but since we are using a function which accepts the currentState, when it eventually fires, the current state will be used to calculate the next state
addOne:
When the button is clicked, a closure is created around addOne where count is captured as the value at the time of the click. If you click the count button 4 times before count has increased, you will have 4 closures of addOne set to be fired, where count is captured as 0.
All 4 timeouts will fire and simply set count to 0 + 1, hence the result of 1 for the count.
Yes, you're right about the origins of this behavior and the other posters here seem to have explained how to fix it. However, I don't see the answer to your specific question:
...but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state Why???
So what you mean is that even if you handle that console.log call like so:
const addOne = () => {
setCount((count) => count + 1);
setTimeout(() => console.log(count), 1000);
}
It will STILL print the old, un-updated value of count. Why? Shouldn't the timeout allow time for count to update? I will quote the answer:
This is subtle but expected behavior. When setTimeout is scheduled it's using the value of count at the time it was scheduled. It's relying on a closure to access count asynchronously. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over.
Source: https://github.com/facebook/react/issues/14010#issuecomment-433788147
So there you have it.

Lodash _.debounce is not canceling existing timer when function is called again in React

I'm trying to debounce the onChange event for a form in my React component. I plan on moving debounceStateUpdate into a static utils function to universalize the debounce time, which is why that extra layer exists rather than just directly using _.debounce.
const ProfileGeneralEditContent = props => {
const debounceStateUpdate = updateFunction => {
return _.debounce(params => updateFunction(params), 700);
};
const FormsyFieldUpdated = debounceStateUpdate((config) => {
console.log("update some things here");
});
return (
<Formsy
onChange={(config) => {
FormsyFieldUpdated.cancel();
FormsyFieldUpdated(config);
}}
onInvalid={() => setValid(false)}
onValid={() => setValid(true)}
>
<div className={'flex justify-start items-start'}>
.
.
. (more jsx)
I would think that when the onChange event fires, the cancel() call would cancel any existing debounce timers that are running and start a new one.
My goal is to debounce inputs from updating state on each key press, so that state will only update after 700ms of no updates. But currently, this code is only delaying each key press' state update by 700 milliseconds, and the state updates for each key press is still happening.
How do I use _.debounce to keep a single running debounce timer for delaying my state update, rather than having 10 timers running at once for each key that is pressed?
I figured it out. I needed to wrap my debounced function definition in useCallback(), because the re-rendering of the component was redefining the debounced function every keypress and thus it would have no knowledge of its previous iterations' running functions.
const ProfileGeneralEditContent = props => {
const debounceStateUpdate = updateFunction => {
return _.debounce(params => updateFunction(params), 700);
};
const FormsyFieldUpdated = useCallback(debounceStateUpdate((config) => {
console.log("update some things here");
}), []);
return (
<Formsy
onChange={(config) => FormsyFieldUpdated(config)}
onInvalid={() => setValid(false)}
onValid={() => setValid(true)}
>
<div className={'flex justify-start items-start'}>
.
.
. (more jsx)

Categories

Resources