简体   繁体   中英

How do I implement debounce in ramda

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 comparison.  Internally this uses
 * [lodash debounce](https://lodash.com/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];
}

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, compressed 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, compressed 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 component is unmounted to prevent any dangling debounced operations -

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

Add useDebounce to your component 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, compressed 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, compressed 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.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/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.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script> <div id="app"></div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM