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 extraif
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
1 Answer
Reset to default 11debounce 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
版权声明:本文标题:javascript - How do I implement debounce in ramda - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741878899a2402631.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论