Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
2 tasks done
dfaust opened this issue May 10, 2024 · 5 comments · Fixed by #6257
Closed
2 tasks done

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

dfaust opened this issue May 10, 2024 · 5 comments · Fixed by #6257

Comments

@dfaust
Copy link
Contributor

dfaust commented May 10, 2024

Dependencies check up

  • 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

  • I would be willing to implement a fix for this issue
@rtivital
Copy link
Member

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

@dfaust
Copy link
Contributor Author

dfaust commented May 11, 2024

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'

@rtivital
Copy link
Member

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

@yshterev
Copy link

yshterev commented May 15, 2024

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

@dfaust
Copy link
Contributor Author

dfaust commented May 19, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants