Skip to content

Commit

Permalink
fix(react): useBlockMultipleClick -> useBlockMultipleAsyncCalls 네이밍 변…
Browse files Browse the repository at this point in the history
…경 및 문서 개선 (#628)
  • Loading branch information
ssi02014 authored Dec 9, 2024
1 parent d7ad2bc commit 52131b7
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-turtles-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-kit/react': minor
---

fix(react): useBlockMultipleClick -> useBlockMultipleAsyncCalls 네이밍 변경 - @ssi02014
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
import { useState } from 'react';
import { useBlockPromiseMultipleClick } from '@modern-kit/react';
import { useBlockMultipleAsyncCalls } from '@modern-kit/react';

# useBlockPromiseMultipleClick
# useBlockMultipleAsyncCalls

인자로 넘겨준 Callback 함수의 `Promise` 동작을 수행하는 동안 `중복 호출이 불가능하도록 차단`하는 커스텀 훅입니다.
`useBlockMultipleAsyncCalls` 훅은 진행 중인 비동기 호출이 있을 때 중복 호출을 방지하기 위한 커스텀 훅입니다.

[useDebounce](https://modern-agile-team.github.io/modern-kit/docs/react/hooks/useDebounce)를 사용해 중복 호출을 방지할 수는 있지만, 시간 값에 의존하기 때문에 만약 `Promise`가 이행될 때까지 호출을 완벽하게 차단해야 한다면 부족합니다.
`debounce`는 함수의 중복 호출을 방지하는 데 대부분의 경우에 효과적입니다.
하지만, `debounce`는 비동기 작업의 완료를 보장하지 않기 때문에 다음과 같은 한계가 있습니다:

해당 훅은 Promise 동작을 수행하는 동안 중복 호출을 방지하기 때문에, `Promise` 이행을 보장하면서 중복 호출을 방지하고 싶을 때 사용할 수 있습니다.
1. `debounce` 시간이 API 응답 시간보다 짧을 경우: 비동기 작업이 완료되지 않은 상태에서 `다시 호출`될 수 있습니다.
2. `debounce` 시간이 API 응답 시간보다 길 경우: 비동기 작업이 완료되었지만 `버튼`과 같은 요소가 여전히 `비활성화`되어 있을 수 있습니다.
3. `즉각적인 반응`을 원하는 경우: `debounce`는 호출을 지연시키기 때문에 사용자에게 `즉각적인 반응`을 보여주기에 제한적입니다.

대부분의 경우에 `debounce`만으로 충분하지만, 위와 같은 한계점을 대응하고자 한다면 `useBlockMultipleAsyncCalls`를 사용할 수 있습니다.

- [useDebounce](https://github.com/modern-agile-team/modern-kit/blob/main/packages/react/src/hooks/useDebounce/index.ts)

<br />

## Code
[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/react/src/hooks/useBlockPromiseMultipleClick/index.ts)
[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/react/src/hooks/useBlockMultipleAsyncCalls/index.ts)

## Interface
```ts title="typescript"
function useBlockPromiseMultipleClick(): {
function useBlockMultipleAsyncCalls(): {
isLoading: boolean;
blockMultipleClick: <T>(callback: () => Promise<T>) => Promise<T | undefined>;
blockMultipleAsyncCalls: <T>(callback: () => Promise<T>) => Promise<T | undefined>;
};
```

## Usage

```tsx title="typescript"
import React, { useState } from 'react';
import { useBlockPromiseMultipleClick } from '@modern-kit/react';
import { useBlockMultipleAsyncCalls } from '@modern-kit/react';

interface Value {
userId: number;
Expand All @@ -40,7 +47,7 @@ const Example = () => {
const [nonBlockingCount, setNonBlockingCount] = useState(1);
const [value, setValue] = useState<Value | null>(null);

const { isLoading, blockMultipleClick } = useBlockPromiseMultipleClick();
const { isLoading, blockMultipleAsyncCalls } = useBlockMultipleAsyncCalls();

const fetchApi = async () => {
const res = await fetch(
Expand All @@ -53,7 +60,7 @@ const Example = () => {

const handleClick = () => {
setNonBlockingCount(nonBlockingCount + 1);
blockMultipleClick(fetchApi); // (*) Promise 반환하는 함수를 인자로 넣어주세요.
blockMultipleAsyncCalls(fetchApi); // (*) Promise 반환하는 함수를 인자로 넣어주세요.
};

return (
Expand All @@ -75,7 +82,7 @@ export const Example = () => {
const [blockingCount, setBlockingCount] = useState(1);
const [nonBlockingCount, setNonBlockingCount] = useState(1);
const [value, setValue] = useState(null);
const { isLoading, blockMultipleClick } = useBlockPromiseMultipleClick();
const { isLoading, blockMultipleAsyncCalls } = useBlockMultipleAsyncCalls();
const fetchApi = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${blockingCount}`
Expand All @@ -85,7 +92,7 @@ export const Example = () => {
};
const handleClick = () => {
setNonBlockingCount(nonBlockingCount + 1);
blockMultipleClick(fetchApi);
blockMultipleAsyncCalls(fetchApi);
};
return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './useAsyncEffect';
export * from './useAsyncProcessQueue';
export * from './useBeforeUnload';
export * from './useBlockPromiseMultipleClick';
export * from './useBlockMultipleAsyncCalls';
export * from './useClipboard';
export * from './useColorScheme';
export * from './useCounter';
Expand Down
71 changes: 71 additions & 0 deletions packages/react/src/hooks/useBlockMultipleAsyncCalls/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useCallback, useRef, useState } from 'react';

interface UseBlockMultipleAsyncCallsReturnType {
isLoading: boolean;
blockMultipleAsyncCalls: <T>(
callback: () => Promise<T>
) => Promise<T | undefined>;
}
/**
* @description `useBlockMultipleAsyncCalls` 훅은 진행 중인 비동기 호출이 있을 때 중복 호출을 방지하기 위한 커스텀 훅입니다.
*
* `debounce`는 함수의 중복 호출을 방지하는 데 대부분의 경우에 효과적입니다.
* 하지만, `debounce`는 비동기 작업의 완료를 보장하지 않기 때문에 다음과 같은 한계가 있습니다:
*
* 1. `debounce` 시간이 API 응답 시간보다 짧을 경우: 비동기 작업이 완료되지 않은 상태에서 `다시 호출`될 수 있습니다.
* 2. `debounce` 시간이 API 응답 시간보다 길 경우: 비동기 작업이 완료되었지만 `버튼`과 같은 요소가 여전히 `비활성화`되어 있을 수 있습니다.
* 3. `즉각적인 반응`을 원하는 경우: `debounce`는 호출을 지연시키기 때문에 사용자에게 `즉각적인 반응`을 보여주기에 제한적입니다.
*
* 대부분의 경우에 `debounce`만으로 충분하지만, 위와 같은 한계점을 대응하고자 한다면 `useBlockMultipleAsyncCalls`를 사용할 수 있습니다.
*
* @returns {UseBlockMultipleAsyncCallsReturnType} 다음을 포함하는 객체:
* - `isLoading`: 현재 비동기 작업이 진행 중인지 나타내는 불리언 값
* - `blockMultipleAsyncCalls`: 비동기 작업을 래핑하여 중복 호출을 방지하는 함수
*
* @example
* ```tsx
* function MyComponent() {
* const { isLoading, blockMultipleAsyncCalls } = useBlockMultipleAsyncCalls();
*
* const fetchApi = async () => {
* const data = await fetchData();
* // 데이터 처리
* };
*
* const handleClick = () => {
* blockMultipleAsyncCalls(fetchApi);
* };
*
* return <button onClick={handleClick} disabled={isLoading}>데이터 불러오기</button>
* }
* ```
*/
export function useBlockMultipleAsyncCalls(): UseBlockMultipleAsyncCallsReturnType {
const [isLoading, setIsLoading] = useState(false);
const isCalled = useRef(false);

const blockMultipleAsyncCalls = useCallback(
async <T>(callback: () => Promise<T>) => {
if (isCalled.current) {
return;
}

isCalled.current = true;
setIsLoading(true);

try {
const result = await callback();
return result;
} finally {
isCalled.current = false;
setIsLoading(false);
}
},
[]
);

return {
isLoading,
blockMultipleAsyncCalls,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { screen, renderHook, waitFor } from '@testing-library/react';
import { renderSetup } from '../../_internal/test/renderSetup';
import { useBlockMultipleAsyncCalls } from '.';
import { delay } from '@modern-kit/utils';

beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
});

afterEach(() => {
vi.useRealTimers();
});

const DELAY_TIME = 1000;

describe('useBlockMultipleAsyncCalls', () => {
it('비동기 작업 완료 전 중복 호출 시 한 번만 실행되어야 합니다', async () => {
const mockFn = vi.fn(async () => await delay(DELAY_TIME));
const { result } = renderHook(useBlockMultipleAsyncCalls);

const { blockMultipleAsyncCalls } = result.current;
expect(result.current.isLoading).toBe(false);

blockMultipleAsyncCalls(mockFn);
blockMultipleAsyncCalls(mockFn);
blockMultipleAsyncCalls(mockFn);

await waitFor(async () => {
expect(result.current.isLoading).toBe(true);
expect(mockFn).toHaveBeenCalledTimes(1);
});

vi.advanceTimersByTime(DELAY_TIME);

await waitFor(async () => {
expect(result.current.isLoading).toBe(false);
});

vi.advanceTimersByTime(DELAY_TIME);

await waitFor(async () => {
expect(mockFn).toHaveBeenCalledTimes(1);
});
});

// 이해를 돕기 위해 컴포넌트 예제 추가
it('버튼을 여러 번 클릭해도 비동기 작업이 완료되기 전까지는 한 번만 실행되어야 합니다.', async () => {
const mockFn = vi.fn(async () => await delay(DELAY_TIME));
const { result } = renderHook(useBlockMultipleAsyncCalls);

const { blockMultipleAsyncCalls } = result.current;
const onClick = () => blockMultipleAsyncCalls(mockFn);

const { user } = renderSetup(<button onClick={onClick}>TestButton</button>);
const button = screen.getByRole('button');

expect(result.current.isLoading).toBe(false);

await user.click(button);
await user.click(button);
await user.click(button);

await waitFor(async () => {
expect(result.current.isLoading).toBe(true);
expect(mockFn).toHaveBeenCalledTimes(1);
});

vi.advanceTimersByTime(DELAY_TIME);

await waitFor(async () => {
expect(result.current.isLoading).toBe(false);
});
});
});
31 changes: 0 additions & 31 deletions packages/react/src/hooks/useBlockPromiseMultipleClick/index.ts

This file was deleted.

This file was deleted.

0 comments on commit 52131b7

Please sign in to comment.