Part of a small project I'm working on is the user being able to add tags to items, similarly to StackOveflow. These tags are stored in a database, so obviously I need to call an API to fetch matching tags. To prevent my API from being hit too often, I want to add a debounce method, however none of the examples I've found seem to work. Even lodash's debounce method doesn't work.
I'm currently trying this debounce method in Vue3;
const debounce = (fn: Function, delay: number): Function => {
let timeout: NodeJS.Timeout;
return (...args: any): void => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
onMounted(() => {
debounce(() => console.log('hello'), 500);
});
The debounce method itself is called just fine, but the provided callback isn't. Same goes for lodash's debounce method, the method itself is called but whatever method I pass isn't.
Am I missing something obvious?
Edit: I was indeed missing something obvious. The actual use case was this method (no API call yet, wanted to get the debounce method working first);
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
};
which is called whenever the keyup event is fired. Simply changing it to
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
findTags(value);
};
const findTags = debounce((value: string) => {
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
}, 500);
fixed the issue.
This isn't how debounce is used. Think about it: how would debounce respond to multiple calls to onMounted if a new debounce is created every time onMounted is called?
debounce returns a function that must be called, and calls to that returned function are denounced:
// In some scope. I don't know Vue.js
const debounced = _.debounce(() => console.log('hello'), 500);
onMounted(() => {
debounced()
});
Related
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();
}
};
My class:
var indexedDBInitInterval; // clearInterval Doesnt work when done from within class for some reason..
var coumter = 1;
class IndexedDBWrapper {
constructor() {
this._db = undefined
this._dbInitInterval = undefined
this.dbInitRequest = indexedDB.open("someDB", 1)
this.dbInitRequest.onerror = (event) => { this.dbInitRequestOnError(event) }
this.dbInitRequest.onsuccess = (event) => { this.dbInitRequestOnSuccess(event) }
this.dbInitRequest.onupgradeneeded = (event) => { this.dbInitRequestOnUpgradedeNeeded(event) }
}
isDBInitalized() {
return new Promise((resolve) => {
indexedDBInitInterval = setInterval(() => {
log(this.dbInitRequest)
log(this.dbInitRequest.readyState)
log(indexedDBInitInterval)
coumter = coumter + 1
if (this.dbInitRequest.readyState == "done") {
log("mutants")
log(coumter)
log(clearInterval(indexedDBInitInterval))
resolve()
}
}, 300)
})
}
dbInitRequestOnError(event) {
log("Error initializing IndexedDB: ")
log(event)
}
And calling with:
indexedDBWrapper.isDBInitalized().then(() => {
Neither clearInterval or resolve gets fired, even tho log("mutants") gets fired.
What a puzzle..
You'd want to make indexedDBInitInterval a variable within isDBInitialized. Otherwise if you call the function multiple (times or even on multiple objects), they would interfere with each other. The same can be said for coumter, although that might just be a debug variable.
Does indexedDBWrapper.isDBInitalized().then(() => console.log('OK')) print OK? I can understan the clearInterval failing if the wrong indexedDBInitInterval is used, but mutants being logged should indicate that resolve() also gets called, unless an error happens in between.
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
I have a form with a handle function attached to it.
The handle function has a timeout and this is causing some problems.
const timeOut = useRef(null);
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(e.target.value.toLowerCase().trim());
}, 500);
}
If I console.log(e.target.value) outside the settimeout function it works fine, when i incorporate the setTimeout function it breaks. Why is this?
I tried simplifying the function to just this :
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
setTimeout(() => {
// setSearchKey(e.target.value.toLowerCase().trim());
console.log(e.target.value)
}, 500);
}
The issue stays..It logs the first console.log and at the second it breaks.
Event values are cleared by react. You either need to use event.persist to persit event values or store the values from event to be used later
According to react documentation:
SyntheticEvent object will be reused and all properties will be
nullified after the event callback has been invoked. This is for
performance reasons. As such, you cannot access the event in an
asynchronous way.
const handleSearchChange = (e) => {
// setSearchKey(e.target.value.toLowerCase().trim());
clearTimeout(timeOut.current);
const value = e.target.value.toLowerCase().trim();
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}
That’s because the e event object in react is a synthetic event object produced by react, not the native event object produced by browser internal.
In order to prevent allocation of new objects all the time, it’s designed to be a reusable object, which means its properties are stripped after emission and re-assigned for next event.
So for your case, because you revisited this object in async callback after emission, it’s been "recycled", making it’s properties outdated. To solve this problem, you can save up beforehand the desired value in the sync event loop, then pass it to async callback.
handleSearchChange = (e) => {
const value = e.target.value.toLowerCase().trim()
clearTimeout(timeOut.current);
timeOut.current = setTimeout(() => {
setSearchKey(value);
}, 500);
}
I have the following code, which doesn't work.
export function makeDraggable(htmlElement: any, dotnetCallbackObject: any): void {
let onDragStart = async (event: any) => {
let mayDrag: boolean = await someAsyncFunction('MayDrag');
if (!mayDrag) {
event.preventDefault();
return false;
}
return true;
};
htmlElement.addEventListener('dragstart', onDragStart);
}
The browser doesn't seem to wait for the async function to complete, and so immediately enables dragging. I want to be able to determine whether drag should be permitted as a result of an async call. Is there a way to achieve this?
Event handlers are synchronous, and need to be, to support bubbling and capturing sanely.
You should eagerly cache the eligibility of dragging and attempt to access that result in your handler instead.
let isDraggable = false
getIsDraggable('maydrag').then((answer) => {
isDraggable = answer
})
const onDragStart = (event) => {
if (!isDraggable) {
event.preventDefault();
}
return isDraggable
}