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

When debounce or throttle options objects have undefined values, those options are unexpectedly set to non-defaults #5857

Open
hashboard-charlie opened this issue May 1, 2024 · 0 comments

Comments

@hashboard-charlie
Copy link

hashboard-charlie commented May 1, 2024

This issue is adapted from #5495 which was closed with Issue Bankruptcy before receiving a reply. This issue is still a problem in the latest versions of lodash.

Background

debounce and throttle take optional options objects:

For debounce:

lodash/lodash.js

Lines 10278 to 10282 in ddfd9b1

* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]

For throttle:

lodash/lodash.js

Lines 10879 to 10883 in ddfd9b1

* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=true]
* Specify invoking on the leading edge of the timeout.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.

Notice that each of these options has sensible defaults.

Bug

If you set these properties to undefined rather than leaving them out of the object, for example:

{
  leading: undefined,
  maxWait: undefined,
  trailing: undefined
}

The options dangerously get set to unexpected values.

For debounce:

  • leading falls back to false
  • maxWait falls back to 0
  • trailing falls back to false

For throttle:

  • leading falls back to false
  • trailing falls back to false

Cause

The cause of this behavior is that the code is checking for settings using the in operator. Instead of 'property' in options, a much better check would be options.property !== undefined.

The buggy code is here:

Debounce:

lodash/lodash.js

Lines 10320 to 10325 in ddfd9b1

if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}

Throttle:

lodash/lodash.js

Lines 10904 to 10907 in ddfd9b1

if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}

Expected Behavior

In JavaScript, we nearly always treat missing properties and undefined properties as the same thing. Even though this is not technically true because an object can have a property with an ndefined` value, it's very surprising when code doesn't do this.

While explicitly calling these functions with explicitly undefined properties is probably very uncommon, it's not uncommon at all to implicitly do so. For example, if a function is wrapping throttle to vend its behavior, one can easily see how this could happen and cause very subtle and unexpected bugs.

Example Use Case

This example is adapted from @k-funk's example in #4973, where a React hook is extending debounce:

function useDebounce(fn, delay, { leading, trailing, maxWait }): {
  // unrelated setup code
  const fnRef = useRef(fn)
  useLayoutEffect(() => fnRef.current = fn, [fn])

  return useMemo(
    // ❗ Note the subtle bug here - leading, trailing, and maxWait could all be set incorrectly!
    () => debounce(() => fnRef.current(), delay, { leading, trailing, maxWait }),
    [leading, trailing, maxWait]
  )
)

Related Issues

Potentially related:

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

No branches or pull requests

1 participant