mantine
mantine copied to clipboard
use-throttled-value does not always issue an update with the last value
Dependencies check up
- [X] I have verified that I use latest version of all @mantine/* packages
What version of @mantine/* packages do you have in package.json?
7.9.0
What package has an issue?
@mantine/hooks
What framework do you use?
Vite
In which browsers you can reproduce the issue?
All
Describe the bug
Currently useThrottledValue (and the other throttled hooks) don't emit an update for the last value change if that change occurred while the timer was active. lodash calls this the trailing edge [1].
Ideally the hooks would support arguments to configure this behavior. But in absence of such arguments, I think emitting on the trailing edge is the more intuitive behavior.
I changed the implementation of useThrottledValue to fit my need.
Let me know if you are interested in a PR. You may also just take my code if you want to.
[1] https://lodash.com/docs/4.17.15#throttle
If possible, include a link to a codesandbox with a minimal reproduction
No response
Possible fix
import { useCallback, useEffect, useRef, useState } from 'react';
export function useThrottledValue<T>(value: T, wait: number) {
const [throttledValue, setThrottledValue] = useState(value);
const valueRef = useRef(value);
const changedValueRef = useRef(value);
const active = useRef(true);
const waitRef = useRef(wait);
const timeoutRef = useRef<number>(-1);
const updateThrottledValue = useCallback((value: T) => {
setThrottledValue(value);
valueRef.current = value;
changedValueRef.current = value;
active.current = false;
}, []);
const timerCallback = useCallback(() => {
if (changedValueRef.current !== valueRef.current) {
updateThrottledValue(changedValueRef.current);
window.clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(timerCallback, waitRef.current);
} else {
active.current = true;
}
}, [updateThrottledValue]);
useEffect(() => {
if (valueRef.current !== value) {
if (active.current) {
updateThrottledValue(value);
window.clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(timerCallback, waitRef.current);
} else {
changedValueRef.current = value;
}
}
}, [timerCallback, updateThrottledValue, value]);
useEffect(() => () => window.clearTimeout(timeoutRef.current), []);
useEffect(() => {
waitRef.current = wait;
}, [wait]);
return throttledValue;
}
Self-service
- [X] I would be willing to implement a fix for this issue
I'm not really sure how this would make the hooks different from debounce hooks
The debounced hooks emit updates only after the data source has settled down. It does not have a leading edge and the emitted update can be postponed indefinitely, as long as the data source keeps changing. This is useful for search boxes, when a request should only be sent, when the user has stopped typing. The throttled hooks have a leading edge and therefore emit an update immediately when the source changes. It also emits updates regularly when the data source keeps changing. This is useful for limiting UI updates when tracking the mouse position and updating the UI based on it. My change guarantees that there will always be an update that reflects the latest value of the data source, regardless of the timing of when the data source has changed.
Here is the behavior using the examples in the documentation:
Debounced:
value: 'abc' (typing 'a', 'b', 'c')
debounced: -
wait
source: 'abc'
debounced: 'abc'
Throttled (old):
value: 'abc' (typing 'a', 'b', 'c')
throttled: 'a'
wait
value: 'abc'
throttled: 'a'
Throttled (new):
value: 'abc' (typing 'a', 'b', 'c')
throttled: 'a'
wait
value: 'abc'
throttled: 'abc'
Okay, I'm fine with these changes, you are welcome to submit a PR
In lodash throttle function leading and trailing are options. It might be a good idea to have them as options in useThrottledValue as well
In lodash
throttlefunctionleadingandtrailingare options. It might be a good idea to have them as options inuseThrottledValueas well
That would be a nicer solution. My PR does not include such a options, though.