admin管理员组

文章数量:1312935

I am pretty sure the answer is that it is not possible, but I was wondering if it is possible to implement lodash.debounce using Ramda so I can get rid of the lodash dependency in my app since it's down to just that.

This is the code I am using

import debounce from "lodash.debounce";
import { Dispatch, useCallback, useState } from "react";


/**
 * This is a variant of set state that debounces rapid changes to a state.
 * This perform a shallow state check, use {@link useDebouncedDeepState}
 * for a deep parison.  Internally this uses
 * [lodash debounce]() to perform
 * the debounce operation.
 * @param initialValue initial value
 * @param wait debounce wait
 * @param debounceSettings debounce settings.
 * @returns state and setter
 *
 */
export function useDebouncedState<S>(
  initialValue: S,
  wait: number,
  debounceSettings?: Parameters<typeof debounce>[2]
): [S, Dispatch<S>] {
  const [state, setState] = useState<S>(initialValue);
  const debouncedSetState = useCallback(
    debounce(setState, wait, debounceSettings),
    [wait, debounceSettings]
  );
  useEffect(()=> {
    return () => debouncedSetState.cancel();
  }, []);
  return [state, debouncedSetState];
}

I am pretty sure the answer is that it is not possible, but I was wondering if it is possible to implement lodash.debounce using Ramda so I can get rid of the lodash dependency in my app since it's down to just that.

This is the code I am using

import debounce from "lodash.debounce";
import { Dispatch, useCallback, useState } from "react";


/**
 * This is a variant of set state that debounces rapid changes to a state.
 * This perform a shallow state check, use {@link useDebouncedDeepState}
 * for a deep parison.  Internally this uses
 * [lodash debounce](https://lodash./docs/#debounce) to perform
 * the debounce operation.
 * @param initialValue initial value
 * @param wait debounce wait
 * @param debounceSettings debounce settings.
 * @returns state and setter
 *
 */
export function useDebouncedState<S>(
  initialValue: S,
  wait: number,
  debounceSettings?: Parameters<typeof debounce>[2]
): [S, Dispatch<S>] {
  const [state, setState] = useState<S>(initialValue);
  const debouncedSetState = useCallback(
    debounce(setState, wait, debounceSettings),
    [wait, debounceSettings]
  );
  useEffect(()=> {
    return () => debouncedSetState.cancel();
  }, []);
  return [state, debouncedSetState];
}
Share Improve this question edited May 31, 2022 at 19:07 Archimedes Trajano asked May 31, 2022 at 18:50 Archimedes TrajanoArchimedes Trajano 41.8k28 gold badges214 silver badges347 bronze badges 4
  • 3 Why not just use plain JavaScript? Can someone explain the "debounce" function in Javascript In what way is Ramda really useful for implementing this pretty standard and well-known function? – VLAZ Commented May 31, 2022 at 18:51
  • the example provided doesn't have the nuances that lodash.debounce does. One of which is cancelation semantics which I just noticed my code does not use and would explain bug I had where there was a state change when the ponent was unmounted. – Archimedes Trajano Commented May 31, 2022 at 18:56
  • 2 I still fail to see how Ramda is relevant to implementing debouncing, though. If you want cancellation, that's still nothing to do with another library. As you can see, debounding just has two really relevant ponents - delay (which you don't need a library for) and calling a function by preserving this (which is also something you don't need a library for). A cancellation mechanic is an extra if in the delay to decide whether to fire the delayed function. For which of these tasks do you want to use Ramda? – VLAZ Commented May 31, 2022 at 19:00
  • 1 Ramda does not include anything like debounce as it doesn't really seem to sit well with Ramda's philosophy. (Disclaimer: I'm a Ramda founder.) But as VLAZ says, it's not difficult to create your own. I doubt Ramda functions would help much with doing that, but you never know! – Scott Sauyet Commented May 31, 2022 at 19:42
Add a ment  | 

1 Answer 1

Reset to default 11

debounce without cancellation

VLAZ linked Can someone explain the "debounce" function in Javascript? but you seem disappointed and looking for something with a cancellation mechanism. The answer I provided to that question implements a vanilla debounce that -

At most one promise pending at any given time (per debounced task)
Stop memory leaks by properly cancelling pending promises
Resolve only the latest promise
Expose cancellation mechanism

We wrote debounce with two parameters, the task to debounce, and the amount of milliseconds to delay, ms. We introduced a single local binding for its local state, t -

// original implementation
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return async (...args) => { // ⚠️ does not return cancel mechanism
    try {
      t.cancel()
      t = deferred(ms)
      await t.promise
      await task(...args)
    }
    catch (_) { /* prevent memory leak */ }
  }
}
// original usage
// ⚠️ how to cancel?
myform.mybutton.addEventListener("click", debounce(clickCounter, 1000))

now with external cancellation

The original code is approachable in size, less than 10 lines, and is intended for you to tinker with to meet your specific needs. We can expose the cancellation mechanism by simply including it with the other returned value -

// revised implementation
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [
    async (...args) => { 
      try {
        t.cancel()
        t = deferred(ms)
        await t.promise
        await task(...args)
      }
      catch (_) { /* prevent memory leak */ }
    },
    _ => t.cancel() // ✅ return cancellation mechanism
  ]
}
// revised usage
const [inc, cancel] = debounce(clickCounter, 1000) // ✅ two controls
myform.mybutton.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)

deferred

debounce depends on a reusable deferred function, which creates a new promise that resolves in ms milliseconds. Read more about it in the linked Q&A -

function deferred(ms) {
  let cancel, promise = new Promise((resolve, reject) => {
    cancel = reject
    setTimeout(resolve, ms)
  })
  return { promise, cancel }
}

demo with cancellation

Run the snippet below. The Click is debounced for one (1) second. After the debounce timer expires, the counter is incremented. However, if you click Cancel while inc is debounced, the pending function will be cancelled and the counter will not be incremented.

// debounce, pressed for demo
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [ async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args) } catch (_) { /* prevent memory leak */ } }, _ => t.cancel() ]
}

// deferred, pressed for demo
function deferred(ms) {
  let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel }
}

// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter

// event handler
function clickCounter (event) {
  mycounter.value = Number(mycounter.value) + 1
}

// debounced listener
[inc, cancel] = debounce(clickCounter, 1000)
myform.myclicker.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)
<form id="myform">
<input name="myclicker" type="button" value="click" />
<input name="mycancel" type="button" value="cancel" />
<output name="mycounter">0</output>
</form>

types

Some sensible annotations for deferred and debounce, for the people thinking about types.

// cancel : () -> void
// 
// waiting : {
//   promise: void promise,
//   cancel: cancel
// }
//
// deferred : int -> waiting
function deferred(ms) {
  let cancel, promise = new Promise((resolve, reject) => {
    cancel = reject
    setTimeout(resolve, ms)
  })
  return { promise, cancel }
}
// 'a task : (...any -> 'a)
//
// debounce : ('a task, int) -> ('a task, cancel)
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [
    async (...args) => { 
      try {
        t.cancel()
        t = deferred(ms)
        await t.promise
        await task(...args)
      }
      catch (_) { /* prevent memory leak */ }
    },
    _ => t.cancel()
  ]
}

react hook

Implementing useDebounce with debounce is super easy. Remember to cancel when the ponent is unmounted to prevent any dangling debounced operations -

function useDebounce(task, ms) {
  const [f, cancel] = debounce(task, ms)
  useEffect(_ => cancel) // ✅ auto-cancel when ponent unmounts
  return [f, cancel]
}

Add useDebounce to your ponent is the same way we used vanilla debounce above. If debouncing state mutations, make sure to use functional updates as setter will be called asynchronously -

function App() {
  const [count, setCount] = React.useState(0)
  const [inc, cancel] = useDebounce(
    _ => setCount(x => x + 1), // ✅ functional update
    1000
  )
  return <div>
    <button onClick={inc}>click</button>
    <button onClick={cancel}>cancel</button>
    <span>{count}</span>
  </div>
}

react debounce demo

This demo is the same as the only above, only use React and our useDebounce hook -

// debounce, pressed for demo
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [ (...args) => { t.cancel(); t = deferred(ms); t.promise.then(_ => task(...args)).catch(_ => {}) }, _ => t.cancel() ]
}

// deferred, pressed for demo
function deferred(ms) {
  let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel }
}

function useDebounce(task, ms) {
  const [f, cancel] = debounce(task, ms)
  React.useEffect(_ => cancel)
  return [f, cancel]
}

function App() {
  const [count, setCount] = React.useState(0)
  const [inc, cancel] = useDebounce(
    _ => setCount(x => x + 1),
    1000
  )
  return <div>
    <button onClick={inc}>click</button>
    <button onClick={cancel}>cancel</button>
    <span>{count}</span>
  </div>
}

ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

multiple debounces

Let's double-check everything is correct and show multiple debounces being used on the same page. We'll extend the counter example by adding more Click buttons that call the same debounced function. And we'll put multiple counters on the same page to show that multiple debouncers maintain individual control and don't interrupt other debouncers. Here's a preview of the app -

Run the demo and verify each of these behaviours -

3 Counters, each with their own counter state
Each counter has 3 debounced Click buttons and a single Cancel button
Each Click can be used to increment the counter's value
Each Click will interrupt any debounced increment from other Click belonging to that counter
The Cancel button will cancel debounced increments from any Click belonging to that counter
Cancel will not cancel debounced increments belonging to other counters

function debounce(task, ms) { let t = { promise: null, cancel: _ => void 0 }; return [ (...args) => { t.cancel(); t = deferred(ms); t.promise.then(_ => task(...args)).catch(_ => {}) }, _ => t.cancel() ] }
function deferred(ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
function useDebounce(task, ms) {const [f, cancel] = debounce(task, ms); React.useEffect(_ => cancel); return [f, cancel] }

function useCounter() {
  const [count, setCount] = React.useState(0)
  const [inc, cancel] = useDebounce(
    _ => setCount(x => x + 1),
    1000
  )
  return [count, <div className="counter">
    <button onClick={inc}>click</button>
    <button onClick={inc}>click</button>
    <button onClick={inc}>click</button>
    <button onClick={cancel}>cancel</button>
    <span>{count}</span>
  </div>]
}

function App() {
  const [a, counterA] = useCounter()
  const [b, counterB] = useCounter()
  const [c, counterC] = useCounter()
  return <div>
    {counterA}
    {counterB}
    {counterC}
    <pre>Total: {a+b+c}</pre>
  </div>
}

ReactDOM.render(<App/>, document.querySelector("#app"))
.counter { padding: 0.5rem; margin-top: 0.5rem; background-color: #ccf; }
pre { padding: 0.5rem; background-color: #ffc; }
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

本文标签: javascriptHow do I implement debounce in ramdaStack Overflow