Debouncing a function not working - REACT functional component - javascript

I am new to React so as part of learning I am trying to do a js debounce function when typed in input box (a search box simulation). But for some reason it is not working. The function is getting called each for key up than once for 2000 ms or delay specified in timeout. Below is the link to sandbox code for your reference. I have referred across blogs, the implementations seems the same yet I could not figure out what is the issue.
https://codesandbox.io/s/react-debouncing-9g7tc?file=/src/search.component.js

Issue :
const debounce = (func, delay) => {
let t; // <-- this will be new variable each time function get called
return function() {
clearTimeout(t); // <--- this also points to the new one, so it will clear nothing
// so all the previously called func will be called
t = setTimeout(() => func(), delay);
};
};
1st Solution : useRef
const t = useRef(null);
const debounce = (func, delay) => {
return function() {
clearTimeout(t.current); // <--- pointing to prev setTimeout or null
t.current = setTimeout(() => func(), delay);
};
};
WORKING DEMO
2nd Solution : Define t outside scope of SearchUi
var t;
const SearchUi = () => {
...
const debounce = (func, delay) => {
return function() {
clearTimeout(t);
t = setTimeout(() => func(), delay);
};
};
...
};
WORKING DEMO

Related

how to trigger a function after a delay only once with the latest value of the textbox

I have react app & need to trigger a function once user stops typing in the textbox. This is what I tried.
const inputHandler =(e:SynthenticEvent) => {
let timer: any = null;
clearTimeout(timer);
timer = setTimeout(() => {
console.log('hi');
//in actual sending an Http request, however hiding it here for brevity
},1000);
}
<input type="text" onChange={inputHandler} name="userInput" />
What's happening as of now, that the inputHandler trigger after 1000ms but it trigger the multiples times (ex 100 is triggering 3 times, 1000 triggers 4 times)
Which is not ideal or expected?
I thought to make use of useEffect, but on doing so inputHandler() is not in identified/scope by textbox?
What I'm looking is at trigger the function after 1000ms but once with
latest value of the texbox?
Thanks!
This called debounce
Debounce function.
Javascript code.
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
}
}
Typescript code.
function debounce<Params extends any[]>(func: Function, timeout =
300) {
let timer: ReturnType<typeof setTimeout>;
return (...args: Params) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this as Function,
args); }, timeout);
}
}
To use it.
const handleChange = debounce(() => console.log('hi'))
<input type="text" onChange={inputHandler} name="userInput" />
Thanks to #Mina for letting me know about debouncing. On googling around debounce in javascript, I found a minor fix in my code which addressed my requirement precisely
I was almost close, all I was doing wrong was that I had defined & initialized the timer inside the function, just moving outside the target function resolved the issue
let timer: any = null; //moved outside the function
const inputHandler =(e:SynthenticEvent) => {
clearTimeout(timer);
timer = setTimeout(() => {
console.log('hi');
//in actual sending an Http request, however hiding it here for brevity
},1000);
}

SetInterval causes too many re-renders React

Hello I would like to put setInterval in my React project to add 1 for each second but I got an error like in title of this post.
js:
const [activeTab, setActiveTab] = useState(0)
useEffect(() => {
setInterval(setActiveTab(prevTab => {
if (prevTab === 3) return 0
console.log('hi')
return prevTab += 1
}), 1000)
})
There are a few issues:
You're not passing a function to setInterval, you're calling setActiveTab and passing its return value into setInterval.
If you were passing in a function, you'd be adding a new repeated timer every time your componennt ran. See the documentation — useEffect:
By default, effects run after every completed render...
And setInterval:
The setInterval() method... repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
(my emphasis)
Starting a new repeating timer every time your component re-renders creates a lot of repeating timers.
Your component will leave the interval timer running if the component is unmounted. It should stop the interval timer.
To fix it, pass in a function, add a dependency array, and add a cleanup callback to stop the interval:
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
const handle = setInterval(() => { // *** A function
setActiveTab(prevTab => {
if (prevTab === 3) return 0;
console.log("?ghi");
return prevTab += 1;
});
}, 1000);
return () => { // *** Clear the interval on unmount
clearInterval(handle); // ***
}; // ***
}, []); // *** Empty dependency array = only run on mount
Side note: Assuming you don't need the console.log, that state setter callback function can be simpler by using the remainder operator:
setActiveTab(prevTab => (prevTab + 1) % 3);
useEffect(() => {
setInterval(() => {
setActiveTab((prevTab) => {
if (prevTab !== 3) {
return (prevTab += 1);
} else {
return 0;
} // console.log("hi");
});
}, 1000);
}, []);

Same logic but different behaviour in 'class' and in 'functional component'

Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.
What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};

I need to call ClearInterval from a different function

I'm creating a countdown timer and I need to call clearInterval from a different function as I want to start and pause the countdown with two different buttons
Here's a piece of my code
const startCountdown = () => {
const countdown = setInterval(() => {
setSecondsElapsed(secondsElapsed => secondsElapsed - 1);
}, 1000);
};
const pauseCountdown = () => {
clearInterval(countdown);
};
The countdown starts when I press the initial button but it doesn't pause when I press the button calling pauseCountdown()
Use a React ref to hold the timer reference. When setting the interval store the countdown reference as the ref's current value, and in the other function clear the interval using the ref's current value.
const countDownRef = React.useRef();
const startCountdown = () => {
countDownRef.current = setInterval(() => {
setSecondsElapsed(secondsElapsed => secondsElapsed - 1);
}, 1000);
};
const pauseCountdown = () => {
clearInterval(countDownRef.current);
};
try declaring countdown globally so it can be accessed from any function. I'd also recommend using var instead of const for things that will be frequently redefined, such as a pausable countdown loop.
try this:
var countdown;
const startCountdown = () => {
countdown = setInterval(() => {
setSecondsElapsed(secondsElapsed => secondsElapsed - 1);
}, 1000);
};
const pauseCountdown = () => {
clearInterval(countdown);
};
The countdown value is inside the scope of startCountdown function hence it can not be accessed from pauseCountdown function which is not inside that very scope.
There are many different ways to do this job properly. I would advise you to be more structural with the help of new abstractions.
I may perhaps use a Countdown class for this job.
class Countdown {
#sid;
#start;
constructor(start){
this.#start = start;
}
startCountdown(){
this.#sid = setInterval( _ => ( !this.#start && clearInterval(this.#sid)
, console.log(this.#start--)
)
, 1000
);
}
pauseCountdown(){
clearInterval(this.#sid);
}
}
var cd = new Countdown(10);
cd.startCountdown();
setTimeout(_ => cd.pauseCountdown(), 5001)
setTimeout(_ => cd.startCountdown(), 10000)
The private class fields #sid and #start keep the clearInterval id and starting value respectively. The thing is they are not exposed to the outer world.

debounce function not working in javascript

I'm having problem understanding why the below debounce code does not work?
you can see the below code in the following: link
`
HTML:
<input type="text" onkeyup="betterFunction(event)"/>
JS:
let newValue;
let counter = 0;
const getData = () => {
// dummy call to API and get Data
console.log("Fetching Data ..", newValue,counter++);
}
const debounce = function (fn, d) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, d);
}
}
const betterFunction = ({target:{value}}) => {
newValue=value;
debounce(getData, 2000); // **line 1**. placing this line of code debouncing does not happen
intermediate() // **line 2**. replacing this line of code with the above line debouncing works
}
const intermediate = debounce(getData, 2000);
`
I understand that the debounce function returns another function which acts like a closure in JavaScript but why the above line 1 code does not work but the line 2 code works
debounce function returns a function which is never called when you call debounce as
debounce(getData, 2000);
dobounce function doesn't needs to return a function. You just need following steps to implement debounce function:
Check if timer is undefined or not. If not, that means there's a timeout that we need to cancel.
After that set a new timer by calling setTimeout() that calls the given function after specific amount of time.
Also, timer should not be a local variable because you don't want it to reset whenever debounce function is called.
let counter = 0;
let newValue;
let timer;
const getData = () => {
console.log("Fetching Data ..", newValue, counter++);
}
const debounce = function(fn, d) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(fn, d);
}
const betterFunction = (e) => {
newValue = e.target.value;
debounce(getData, 2000);
}
<input type="text" onkeyup="betterFunction(event)" />
If you don't want to declare timer as a global variable and want to return a function from debounce function, then you need to call the debounce function once initially and whenever keyup event fires on the input element, you call the function returned from the debounce function instead of calling the debounce function.
let counter = 0;
let newValue;
const getData = () => {
console.log('Fetching Data ..', newValue, counter++);
};
const debounce = function(fn, d) {
let timer;
return function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(fn, d);
};
};
const intermediate = debounce(getData, 2000);
const betterFunction = (e) => {
newValue = e.target.value;
intermediate();
};
<input type="text" onkeyup="betterFunction(event)" />
i hope that what you want :
let counter = 0;
// you need to define timer and newValue here first
let timer , newValue;
// defining input as varible for good usage better than usage in html
var input = document.querySelector("#inp");
const getData = () => {
// increment first better than in console :)
counter+=1;
console.log("Fetching Data .." , newValue , counter);
// as last step clear timer for next timeout
clearTimeout(timer);
}
// givin value direct to timer directlly worked better than return
const debounce = function (fn, d) {
timer = setTimeout(fn, d);
}
const betterFunction = () => {
// newvalue must equal input value
newValue = input.value;
// and then calling debounce as last step
debounce(getData, 2000);
}
// here giving onkeyup event to input for getting values and start working :)
input.onkeyup = betterFunction;

Categories

Resources