mantine icon indicating copy to clipboard operation
mantine copied to clipboard

use-throttled-value does not always issue an update with the last value

Open dfaust opened this issue 1 year ago • 4 comments

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

dfaust avatar May 10 '24 18:05 dfaust

I'm not really sure how this would make the hooks different from debounce hooks

rtivital avatar May 11 '24 06:05 rtivital

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'

dfaust avatar May 11 '24 12:05 dfaust

Okay, I'm fine with these changes, you are welcome to submit a PR

rtivital avatar May 11 '24 13:05 rtivital

In lodash throttle function leading and trailing are options. It might be a good idea to have them as options in useThrottledValue as well

yshterev avatar May 15 '24 11:05 yshterev

In lodash throttle function leading and trailing are options. It might be a good idea to have them as options in useThrottledValue as well

That would be a nicer solution. My PR does not include such a options, though.

dfaust avatar May 19 '24 19:05 dfaust