From 6bf344bc5fc47361990ff69e23839f43bd4f0d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A0=95=ED=99=98/=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=86=A0=ED=83=80=EC=9E=85=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=ED=8C=80?= Date: Mon, 21 Apr 2025 23:44:01 +0900 Subject: [PATCH] feat(useCounter): add useCounter hook --- src/hooks/useCounter/index.ts | 1 + src/hooks/useCounter/ko/useCounter.md | 120 +++++++++++++++ src/hooks/useCounter/useCounter.md | 120 +++++++++++++++ src/hooks/useCounter/useCounter.spec.ts | 187 ++++++++++++++++++++++++ src/hooks/useCounter/useCounter.ts | 100 +++++++++++++ src/index.ts | 1 + 6 files changed, 529 insertions(+) create mode 100644 src/hooks/useCounter/index.ts create mode 100644 src/hooks/useCounter/ko/useCounter.md create mode 100644 src/hooks/useCounter/useCounter.md create mode 100644 src/hooks/useCounter/useCounter.spec.ts create mode 100644 src/hooks/useCounter/useCounter.ts diff --git a/src/hooks/useCounter/index.ts b/src/hooks/useCounter/index.ts new file mode 100644 index 00000000..1e13a46a --- /dev/null +++ b/src/hooks/useCounter/index.ts @@ -0,0 +1 @@ +export { useCounter } from './useCounter.ts'; diff --git a/src/hooks/useCounter/ko/useCounter.md b/src/hooks/useCounter/ko/useCounter.md new file mode 100644 index 00000000..71b15d0b --- /dev/null +++ b/src/hooks/useCounter/ko/useCounter.md @@ -0,0 +1,120 @@ +# useCounter + +`useCounter`는 증가, 감소, 초기화 기능을 갖춘 숫자형 카운터 상태를 관리하는 리액트 훅이에요. 선택적으로 최소값과 최대값을 제공하여 카운터의 범위를 제한할 수 있어요. + +## 인터페이스 + +```ts +function useCounter(options?: UseCounterOptions): UseCounterReturn; + +type UseCounterOptions = { + initialValue?: number; + min?: number; + max?: number; + step?: number; +}; +ㄴ +type UseCounterReturn = { + count: number; + increment: () => void; + decrement: () => void; + reset: () => void; + setCount: (value: number | ((prev: number) => number)) => void; +}; +``` + +### 파라미터 + + + +### 반환 값 + + + +## 예시 + +```tsx +import { useCounter } from 'react-simplikit'; + +function ShoppingCart() { + const { count, increment, decrement, reset } = useCounter({ + initialValue: 1, + min: 1, + max: 10, + }); + + return ( +
+ 수량: {count} + + + +
+ ); +} +``` + +## 제약 조건 + +이 훅은 카운터가 지정된 범위 내에서 유지되도록 자동으로 보장해요: + +- 값이 `max`보다 커지면 자동으로 `max` 값으로 제한돼요. +- 값이 `min`보다 작아지면 자동으로 `min` 값으로 제한돼요. +- `setCount`를 사용할 때 범위를 벗어나는 값은 자동으로 가장 가까운 경계값으로 조정돼요. diff --git a/src/hooks/useCounter/useCounter.md b/src/hooks/useCounter/useCounter.md new file mode 100644 index 00000000..fa5ed032 --- /dev/null +++ b/src/hooks/useCounter/useCounter.md @@ -0,0 +1,120 @@ +# useCounter + +`useCounter` is a React hook that manages a numeric counter state with increment, decrement, and reset capabilities. Optionally, you can provide minimum and maximum values to constrain the counter's range. + +## Interface + +```ts +function useCounter(options?: UseCounterOptions): UseCounterReturn; + +type UseCounterOptions = { + initialValue?: number; + min?: number; + max?: number; + step?: number; +}; + +type UseCounterReturn = { + count: number; + increment: () => void; + decrement: () => void; + reset: () => void; + setCount: (value: number | ((prev: number) => number)) => void; +}; +``` + +### Parameters + + + +### Return Value + + + +## Example + +```tsx +import { useCounter } from 'react-simplikit'; + +function ShoppingCart() { + const { count, increment, decrement, reset } = useCounter({ + initialValue: 1, + min: 1, + max: 10, + }); + + return ( +
+ Quantity: {count} + + + +
+ ); +} +``` + +## Constraints + +The hook automatically ensures that the counter stays within the specified bounds: + +- When incrementing beyond `max`, the value will stay at `max` +- When decrementing below `min`, the value will stay at `min` +- When using `setCount`, any value outside the bounds will be adjusted to the nearest boundary diff --git a/src/hooks/useCounter/useCounter.spec.ts b/src/hooks/useCounter/useCounter.spec.ts new file mode 100644 index 00000000..6141d1af --- /dev/null +++ b/src/hooks/useCounter/useCounter.spec.ts @@ -0,0 +1,187 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { useCounter } from './useCounter.ts'; + +describe('useCounter', () => { + it('should initialize with default value', () => { + const { result } = renderHook(() => useCounter()); + + expect(result.current.count).toBe(0); + }); + + it('should initialize with provided initial value', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 10, + }) + ); + + expect(result.current.count).toBe(10); + }); + + it('should increment the counter', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + }) + ); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(6); + }); + + it('should decrement the counter', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + }) + ); + + act(() => { + result.current.decrement(); + }); + + expect(result.current.count).toBe(4); + }); + + it('should reset the counter to initial value', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + }) + ); + + act(() => { + result.current.increment(); + result.current.increment(); + }); + + expect(result.current.count).toBe(7); + + act(() => { + result.current.reset(); + }); + + expect(result.current.count).toBe(5); + }); + + it('should not go below minimum value', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + min: 3, + }) + ); + + act(() => { + result.current.decrement(); + result.current.decrement(); + result.current.decrement(); + }); + + expect(result.current.count).toBe(3); + }); + + it('should not go above maximum value', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + max: 7, + }) + ); + + act(() => { + result.current.increment(); + result.current.increment(); + result.current.increment(); + }); + + expect(result.current.count).toBe(7); + }); + + it('should use the provided step value for increment and decrement', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + step: 2, + }) + ); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(7); + + act(() => { + result.current.decrement(); + }); + + expect(result.current.count).toBe(5); + }); + + it('should adjust initial value to match constraints', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 1, + min: 3, + }) + ); + + expect(result.current.count).toBe(3); + + const { result: result2 } = renderHook(() => + useCounter({ + initialValue: 10, + max: 8, + }) + ); + + expect(result2.current.count).toBe(8); + }); + + it('should allow setting arbitrary value within constraints', () => { + const { result } = renderHook(() => + useCounter({ + min: 3, + max: 8, + }) + ); + + act(() => { + result.current.setCount(6); + }); + + expect(result.current.count).toBe(6); + + act(() => { + result.current.setCount(1); + }); + + expect(result.current.count).toBe(3); + + act(() => { + result.current.setCount(10); + }); + + expect(result.current.count).toBe(8); + }); + + it('should work with updater function for setCount', () => { + const { result } = renderHook(() => + useCounter({ + initialValue: 5, + }) + ); + + act(() => { + result.current.setCount(prev => prev + 3); + }); + + expect(result.current.count).toBe(8); + }); +}); diff --git a/src/hooks/useCounter/useCounter.ts b/src/hooks/useCounter/useCounter.ts new file mode 100644 index 00000000..4a132808 --- /dev/null +++ b/src/hooks/useCounter/useCounter.ts @@ -0,0 +1,100 @@ +import { useCallback, useState } from 'react'; + +type UseCounterOptions = { + initialValue?: number; + min?: number; + max?: number; + step?: number; +}; + +type UseCounterReturn = { + count: number; + increment: () => void; + decrement: () => void; + reset: () => void; + setCount: (value: number | ((prev: number) => number)) => void; +}; + +/** + * @description + * `useCounter` is a React hook that manages a numeric counter state with increment, decrement, and reset capabilities. + * Optionally, you can provide minimum and maximum values to constrain the counter's range. + * + * @param {UseCounterOptions} options - The options for the counter. + * @param {number} [options.initialValue=0] - Initial value for the counter. Defaults to 0. + * @param {number} [options.min] - Minimum value the counter can reach. If not provided, there is no lower limit. + * @param {number} [options.max] - Maximum value the counter can reach. If not provided, there is no upper limit. + * @param {number} [options.step=1] - Value to increment or decrement by. Defaults to 1. + * + * @returns {UseCounterReturn} An object with count value and control functions. + * + * @example + * import { useCounter } from 'react-simplikit'; + * + * function ShoppingCart() { + * const { count, increment, decrement, reset } = useCounter({ + * initialValue: 1, + * min: 1, + * max: 10, + * }); + * + * return ( + *
+ * Quantity: {count} + * + * + * + *
+ * ); + * } + */ +export function useCounter({ initialValue = 0, min, max, step = 1 }: UseCounterOptions = {}): UseCounterReturn { + const validateValue = (value: number): number => { + let validatedValue = value; + + if (min !== undefined && validatedValue < min) { + validatedValue = min; + } + + if (max !== undefined && validatedValue > max) { + validatedValue = max; + } + + return validatedValue; + }; + + const [count, setCountState] = useState(() => validateValue(initialValue)); + + const validateValueMemoized = useCallback(validateValue, [min, max]); + + const setCount = useCallback( + (value: number | ((prev: number) => number)) => { + setCountState(prev => { + const nextValue = typeof value === 'function' ? value(prev) : value; + + return validateValueMemoized(nextValue); + }); + }, + [validateValueMemoized] + ); + + const increment = useCallback(() => { + setCount(prev => prev + step); + }, [setCount, step]); + + const decrement = useCallback(() => { + setCount(prev => prev - step); + }, [setCount, step]); + + const reset = useCallback(() => { + setCount(initialValue); + }, [setCount, initialValue]); + + return { + count, + increment, + decrement, + reset, + setCount, + }; +} diff --git a/src/index.ts b/src/index.ts index 36bed094..a6bc83c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { SwitchCase } from './components/SwitchCase/index.ts'; export { useAsyncEffect } from './hooks/useAsyncEffect/index.ts'; export { useBooleanState } from './hooks/useBooleanState/index.ts'; export { useCallbackOncePerRender } from './hooks/useCallbackOncePerRender/index.ts'; +export { useCounter } from './hooks/useCounter/index.ts'; export { useDebounce } from './hooks/useDebounce/index.ts'; export { useImpressionRef } from './hooks/useImpressionRef/index.ts'; export { useInputState } from './hooks/useInputState/index.ts';