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

[feat]: useLockFn #164

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ features:
- title: useWindowSize
details: Hook that manages a window size
link: /functions/hooks/useWindowSize
- title: useLockFn
details: A hook that creates a function which ensures that only one instance of the given asynchronous function can run at a time.
link: /functions/hooks/useLockFn
---


32 changes: 32 additions & 0 deletions src/hooks/useLockFn/useLockFn.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react';

import { useLockFn } from '@/hooks/useLockFn/useLockFn';

const mockApiRequest = () => {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 2000);
});
};

const Demo = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('');
const submit = useLockFn(async () => {
setMessage('Start to submit');
await mockApiRequest();
setCount((val) => val + 1);
setMessage('Submit finished');
});

return (
<>
<p>Submit count: {count}</p>
{message && <p>Message: {message}</p>}
<button onClick={submit}>Submit</button>
</>
);
};

export default Demo;
75 changes: 75 additions & 0 deletions src/hooks/useLockFn/useLockFn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { MutableRefObject } from 'react';
import { useCallback, useRef, useState } from 'react';
import type { RenderHookResult } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';

import { useLockFn } from '@/hooks/useLockFn/useLockFn';

type HookReturnType = {
locked: (step: number) => Promise<void | undefined>;
countRef: MutableRefObject<number>;
updateTag: () => void;
};

const sleep = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, time);
});

describe('useLockFn', () => {
const setUp = (): RenderHookResult<HookReturnType, void> =>
renderHook<HookReturnType, void>(() => {
const [tag, updateTag] = useState(false);
const countRef = useRef(0);
const persistFn = useCallback(
async (step: number) => {
countRef.current += step;
await sleep(50);
},
[tag]
);
const locked = useLockFn(persistFn);

return {
locked,
countRef,
updateTag: () => updateTag(true)
};
});

it('should work', async () => {
const hook = setUp();
const { locked, countRef } = hook.result.current;
act(() => {
locked(1);
});
expect(countRef.current).toBe(1);
act(() => {
locked(2);
});
expect(countRef.current).toBe(1);
await sleep(30);
act(() => {
locked(3);
});
expect(countRef.current).toBe(1);
await sleep(30);
act(() => {
locked(4);
});
expect(countRef.current).toBe(5);
act(() => {
locked(5);
});
expect(countRef.current).toBe(5);
});

it('should same', () => {
const hook = setUp();
const preLocked = hook.result.current.locked;
hook.rerender();
expect(hook.result.current.locked).toEqual(preLocked);
act(hook.result.current.updateTag);
expect(hook.result.current.locked).not.toEqual(preLocked);
});
});
36 changes: 36 additions & 0 deletions src/hooks/useLockFn/useLockFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback, useRef } from 'react';

/**
* @name useLockFn
* @description A hook that creates a function which ensures that only one instance of the given asynchronous function can run at a time.
*
* @template P - The parameters type of the function.
* @template V - The return type of the function.
* @param {(...args: P) => Promise<V>} fn - The asynchronous function to be locked.
* @returns {(...args: P) => Promise<V | undefined>} A function that ensures only one instance of the given function can run at a time.
*
* @example
* const lockedFunction = useLockFn(asyncFunction);
* lockedFunction(arg1, arg2).then(result => {
* console.log(result);
* }).catch(error => {
* console.error(error);
* });
*/

export const useLockFn = <P extends any[] = [], V = void>(fn: (...args: P) => Promise<V>) => {
const lockRef = useRef<boolean>(false);

return useCallback(
async (...args: P): Promise<V | undefined> => {
if (lockRef.current) return;
lockRef.current = true;
try {
return await fn(...args);
} finally {
lockRef.current = false;
}
},
[fn]
);
};