30% offEnding soon
useLockFnLoading saved progress…

useLockFn

useLockFn wraps an async function so it cannot overlap with itself: while one call is still in flight, any further call is ignored until the first one settles. It is the standard fix for the double-submit bug — a Save, Like, or Pay button that fires its handler twice because the user clicked (or double-clicked) faster than the request came back. This is the ahooks hook of the same name.

You are given an async function. Return a wrapped version that runs at most one call at a time and drops the rest.

Signature

function useLockFn<Args extends any[], R>(
  asyncFn: (...args: Args) => Promise<R>
): (...args: Args) => Promise<R | undefined>;

The wrapped function takes the same arguments as asyncFn. It resolves to asyncFn's value for a call that actually runs, and to undefined for a call that was ignored.

Examples

A single call runs the function and forwards its result:

const save = useLockFn(saveDraft);
await save({ title: 'Hi' }); // runs saveDraft, resolves to its value

A second call fired while the first is still pending is dropped — saveDraft runs once, not twice:

const save = useLockFn(saveDraft); // saveDraft takes 300ms
save();        // call 1: runs saveDraft
save();        // call 2: still pending → ignored, resolves to undefined
// ...after call 1 settles...
save();        // call 3: lock released → runs saveDraft again

Notes

  • In flight means unsettled. The lock is held from the first call until its promise resolves or rejects — then it releases.
  • Ignored, not queued. A call made while locked never invokes asyncFn and resolves to undefined. It is dropped, not deferred and replayed later.
  • A rejection must still release. If the in-flight call fails, the lock has to open again — otherwise one failed request freezes the button forever.
  • Same tick is the hard case. A double-click fires two calls in one tick, before React re-renders. The flag that guards the second call has to be readable synchronously.
  • Stable identity. The wrapped function keeps one identity across renders, so it is safe to pass to memoized children or list in a dependency array.
  • Out of scope. No cancellation, no timeout, no retry — see the solution's "Going further".