Skip to content

feat: useDebounce, useThrottle 훅 기능 추가 #16

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

Merged
merged 6 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,48 @@ export default Component() {
}
```

### useDebounce

A hook that makes debounce easy to use.
No matter how many times a returned function is called, it is performed only once after a specified time after the function stops being called.

```ts
const callback = () => {};
const debounce = useDebounce(() => callback, 1000); // 1000ms
```

#### Function Arguments

`callback`: The function to be executed at the end via debounce.

`time` : The delay time to perform a callback after a function call has stopped, in ms.

#### Return value

`debounceFunction` : Returns callback with debounce.

### useThrottle

A hook that makes it easy to use the trottle.
No matter how many times the returned function is called, it is performed once per specified time.

```ts
...
const callback=() => {};
const throttle=useThrottle(callback, 1000); // 1000ms
return <button onClick={throttle}>Click me!</button>
```

#### Function Arguments

`callback` : The callback function to be performed by applying a triple.

`time` : Specifies how often to perform the throttle (in ms)

#### Return value

`throttleFunction` : Returns the callback with the throttle applied.

## Animation

The animation of this package is based on ClassName by default.
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import useScrollRatio from './useScrollRatio';
import useInterval from './useInterval/useInterval';
import useAfterMountEffect from './useAfterMountEffect/useAfterMountEffect';
import useRadio from './useRadio/useRadio';
import useThrottle from './useThrottle/useThrottle';
import useDebounce from './useDebounce/useDebounce';

export {
useInput,
Expand All @@ -18,4 +20,6 @@ export {
useInterval,
useAfterMountEffect,
useRadio,
useThrottle,
useDebounce,
};
23 changes: 23 additions & 0 deletions src/stories/useDebounce/Debounce.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import Debounce from './Debounce';

const meta = {
title: 'hooks/useDebounce',
component: Debounce,
parameters: {
layout: 'centered',
docs: {
canvas: {},
},
},
} satisfies Meta<typeof Debounce>;

export default meta;

type Story = StoryObj<typeof meta>;

export const defaultStory: Story = {
args: {
time: 500,
},
};
31 changes: 31 additions & 0 deletions src/stories/useDebounce/Debounce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState } from 'react';
import useDebounce from '../../useDebounce/useDebounce';

export default function Debounce({ time }: { time: number }) {
const [counter, setCounter] = useState(0);
const [realCounter, setRealCounter] = useState(0);

const debounce = useDebounce(() => setCounter((prev) => prev + 1), time);

const handleClick = () => {
debounce();
setRealCounter((prev) => prev + 1);
};

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: 10,
}}
>
<div>
{realCounter}번 눌렀는데, {counter}회 실행되었습니다.
</div>
<button onClick={handleClick}>Click Me!</button>
</div>
);
}
23 changes: 23 additions & 0 deletions src/stories/useDebounce/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Canvas, Meta, Description } from '@storybook/blocks';
import * as Debounce from './Debounce.stories';

<Meta of={Debounce} />

# useDebounce

debounce를 간편하게 사용할 수 있게 하는 훅입니다.
반환된 함수를 아무리 많이 호출하여도 해당 함수의 호출이 멈춘 후 지정된 시간 후에 단 1회 수행됩니다.

## 함수 인자

`callback`: debounce 통해 마지막에 실행시킬 함수입니다.

`time` : 함수 호출이 멈춘 후에 callback을 수행시킬 지연 시간입니다. 단위는 ms입니다.

## 반환값

`debounceFunction` : debounce가 적용된 callback이 반환됩니다.

아래 예시에서는 500ms단위로 debounce가 적용됩니다.

<Canvas of={Debounce.defaultStory} />
29 changes: 29 additions & 0 deletions src/stories/useThrottle/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Canvas, Meta, Description } from '@storybook/blocks';
import * as Throttle from './Throttle.stories';

<Meta of={Throttle} />

# useThrottle

throttle을 간편하게 사용할 수 있게 하는 훅입니다.
반환된 함수를 아무리 많이 호출하여도 지정한 시간당 1회 수행됩니다.

```typescript
...
const throttle=useThrottle(callback, 1000);
return <button onClick={throttle}>Click me!</button>
```

## 함수 인자

`callback` : throttle을 적용하여 수행할 콜백 함수입니다.

`time` : 얼마 간격으로 throttle을 수행할지 시간을 지정합니다. (ms단위)

## 반환값

`throttleFunction` : throttle이 적용된 callback이 반환됩니다.

아래 예시에서는 500ms단위로 throttle이 적용됩니다.

<Canvas of={Throttle.defaultStory} />
23 changes: 23 additions & 0 deletions src/stories/useThrottle/Throttle.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import Throttle from './Throttle';

const meta = {
title: 'hooks/useThrottle',
component: Throttle,
parameters: {
layout: 'centered',
docs: {
canvas: {},
},
},
} satisfies Meta<typeof Throttle>;

export default meta;

type Story = StoryObj<typeof meta>;

export const defaultStory: Story = {
args: {
time: 500,
},
};
30 changes: 30 additions & 0 deletions src/stories/useThrottle/Throttle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useState } from 'react';
import useThrottle from '../../useThrottle/useThrottle';

export default function Throttle({ time }: { time: number }) {
const [counter, setCounter] = useState(0);
const [realCounter, setRealCounter] = useState(0);

const throttle = useThrottle(() => setCounter((prev) => prev + 1), time);

const handleClick = () => {
throttle();
setRealCounter((prev) => prev + 1);
};
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: 10,
}}
>
<div>
{realCounter}번 눌렀는데, {counter}회 실행되었습니다.
</div>
<button onClick={handleClick}>Click Me!</button>
</div>
);
}
39 changes: 39 additions & 0 deletions src/useDebounce/useDebounce.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { act, renderHook } from '@testing-library/react';
import useDebounce from './useDebounce';

const DELAY_TIME = 1000;

beforeAll(() => jest.useFakeTimers());

describe('useDebounce 기능 테스트', () => {
it('debounce 함수는 몇번이 수행되어도 지정된 초 이후에 한번 수행된다.', () => {
const callback = jest.fn();
const { result } = renderHook(() => useDebounce(callback, DELAY_TIME));
Array.from({ length: 1000 }).forEach(() => result.current());
expect(callback).not.toHaveBeenCalled();
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback).toHaveBeenCalledTimes(1);
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback).toHaveBeenCalledTimes(1);
});

it('훅을 여러개를 사용해도 서로 영향을 미치지 않는다.', () => {
const callback = jest.fn();
const callback2 = jest.fn();

const { result } = renderHook(() => useDebounce(callback, DELAY_TIME));
const { result: result2 } = renderHook(() => useDebounce(callback2, DELAY_TIME));

Array.from({ length: 1000 }).forEach(() => result.current());
expect(callback).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();

Array.from({ length: 1000 }).forEach(() => result2.current());
expect(callback2).not.toHaveBeenCalled();
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback2).toHaveBeenCalledTimes(1);
});
});
13 changes: 13 additions & 0 deletions src/useDebounce/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useCallback, useRef } from 'react';

export default function useDebounce(callback: () => void, time: number) {
const ref = useRef<NodeJS.Timeout | null>(null);

const debounceFunction = useCallback(() => {
if (ref.current) clearTimeout(ref.current);

ref.current = setTimeout(callback, time);
}, [callback]);

return debounceFunction;
}
23 changes: 23 additions & 0 deletions src/useThrottle/useThrottle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { renderHook, act } from '@testing-library/react';
import useThrottle from './useThrottle';

const DELAY_TIME = 1000;

describe('useThrottle 기능 테스트', () => {
beforeAll(() => {
jest.useFakeTimers();
});

it('throttle Function이 많이 호출되어도, 지정한 시간 간격에 맞춰서 수행할 수 있다.', () => {
const callback = jest.fn();
const { result } = renderHook(() => useThrottle(callback, DELAY_TIME));
Array.from({ length: 100 }).forEach(() => result.current());
expect(callback).not.toHaveBeenCalled();
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback).toHaveBeenCalledTimes(1);

Array.from({ length: 100 }).forEach(() => result.current());
act(() => jest.advanceTimersByTime(DELAY_TIME));
expect(callback).toHaveBeenCalledTimes(2);
});
});
17 changes: 17 additions & 0 deletions src/useThrottle/useThrottle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useCallback, useRef } from 'react';

export default function useThrottle(callback: () => void, time: number) {
const ref = useRef<NodeJS.Timeout | null>(null);

const throttleFunction = useCallback(() => {
if (ref.current) return;

ref.current = setTimeout(() => {
clearTimeout(ref.current!);
ref.current = null;
callback();
}, time);
}, [callback]);

return throttleFunction;
}
Loading