From 4ee9b53e4361e6083e314b5e01bc54e8b1cd9d72 Mon Sep 17 00:00:00 2001 From: SiHyunLee Date: Thu, 1 Oct 2026 15:50:12 +0900 Subject: [PATCH 01/10] refactor(hooks): standardize option and parameter names (#549) --- .changeset/curly-melons-shop.md | 7 ++++ .../references/useDoubleClick.md | 10 +++--- .../react-simplikit/references/useList.md | 4 +-- .../react-simplikit/references/useMap.md | 6 ++-- .../react-simplikit/references/useSet.md | 6 ++-- .../hooks/useDoubleClick/es/useDoubleClick.md | 10 +++--- .../hooks/useDoubleClick/ja/useDoubleClick.md | 10 +++--- .../hooks/useDoubleClick/ko/useDoubleClick.md | 10 +++--- .../hooks/useDoubleClick/useDoubleClick.md | 10 +++--- .../useDoubleClick/useDoubleClick.spec.tsx | 18 +++++------ .../hooks/useDoubleClick/useDoubleClick.ts | 32 +++++++++---------- .../useDoubleClick/zh-Hans/useDoubleClick.md | 10 +++--- .../src/hooks/useList/es/useList.md | 4 +-- .../src/hooks/useList/ja/useList.md | 4 +-- .../src/hooks/useList/ko/useList.md | 4 +-- .../src/hooks/useList/useList.md | 4 +-- .../src/hooks/useList/useList.ts | 10 +++--- .../src/hooks/useList/zh-Hans/useList.md | 4 +-- .../src/hooks/useMap/es/useMap.md | 6 ++-- .../src/hooks/useMap/ja/useMap.md | 6 ++-- .../src/hooks/useMap/ko/useMap.md | 6 ++-- .../src/hooks/useMap/useMap.md | 6 ++-- .../src/hooks/useMap/useMap.ts | 14 ++++---- .../src/hooks/useMap/zh-Hans/useMap.md | 6 ++-- .../src/hooks/useSet/es/useSet.md | 6 ++-- .../src/hooks/useSet/ja/useSet.md | 6 ++-- .../src/hooks/useSet/ko/useSet.md | 6 ++-- .../src/hooks/useSet/useSet.md | 6 ++-- .../src/hooks/useSet/useSet.ts | 10 +++--- .../src/hooks/useSet/zh-Hans/useSet.md | 6 ++-- 30 files changed, 127 insertions(+), 120 deletions(-) create mode 100644 .changeset/curly-melons-shop.md diff --git a/.changeset/curly-melons-shop.md b/.changeset/curly-melons-shop.md new file mode 100644 index 00000000..16ee5643 --- /dev/null +++ b/.changeset/curly-melons-shop.md @@ -0,0 +1,7 @@ +--- +'react-simplikit': major +--- + +Rename the useDoubleClick options click, doubleClick, and delay to onClick, onDoubleClick, and delayMs, respectively. + +Rename the initial value parameter of useList, useMap, and useSet from initialState to initialValue. diff --git a/packages/plugin/skills/react-simplikit/references/useDoubleClick.md b/packages/plugin/skills/react-simplikit/references/useDoubleClick.md index 61b45ee6..6ea8acde 100644 --- a/packages/plugin/skills/react-simplikit/references/useDoubleClick.md +++ b/packages/plugin/skills/react-simplikit/references/useDoubleClick.md @@ -20,7 +20,7 @@ function useDoubleClick( description="Configuration options for click handling." :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -28,13 +28,13 @@ function useDoubleClick( 'The number of milliseconds to wait before triggering the single click callback. Defaults to 250ms.', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: 'The callback function to be executed on a single click.', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -61,8 +61,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('Zoom in!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return ( diff --git a/packages/plugin/skills/react-simplikit/references/useList.md b/packages/plugin/skills/react-simplikit/references/useList.md index ebb419a4..dd859ef6 100644 --- a/packages/plugin/skills/react-simplikit/references/useList.md +++ b/packages/plugin/skills/react-simplikit/references/useList.md @@ -6,12 +6,12 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### Parameters - + ### Return Value diff --git a/packages/plugin/skills/react-simplikit/references/useMap.md b/packages/plugin/skills/react-simplikit/references/useMap.md index ab50cf3d..74af5ffc 100644 --- a/packages/plugin/skills/react-simplikit/references/useMap.md +++ b/packages/plugin/skills/react-simplikit/references/useMap.md @@ -6,16 +6,16 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### Parameters ### Return Value diff --git a/packages/plugin/skills/react-simplikit/references/useSet.md b/packages/plugin/skills/react-simplikit/references/useSet.md index 3701fbba..f9745fac 100644 --- a/packages/plugin/skills/react-simplikit/references/useSet.md +++ b/packages/plugin/skills/react-simplikit/references/useSet.md @@ -6,15 +6,15 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### Parameters ### Return Value diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/es/useDoubleClick.md b/packages/react-simplikit/src/hooks/useDoubleClick/es/useDoubleClick.md index 135d05a2..b7bb1f2f 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/es/useDoubleClick.md +++ b/packages/react-simplikit/src/hooks/useDoubleClick/es/useDoubleClick.md @@ -20,7 +20,7 @@ function useDoubleClick( description="Opciones de configuración para gestionar los clics." :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -28,13 +28,13 @@ function useDoubleClick( 'El número de milisegundos de espera antes de ejecutar el callback de un solo clic. El valor predeterminado es 250 ms.', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: 'El callback que debe ejecutarse al hacer un solo clic.', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -61,8 +61,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('¡Ampliar!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/ja/useDoubleClick.md b/packages/react-simplikit/src/hooks/useDoubleClick/ja/useDoubleClick.md index 62e6a21a..cc276d75 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/ja/useDoubleClick.md +++ b/packages/react-simplikit/src/hooks/useDoubleClick/ja/useDoubleClick.md @@ -20,7 +20,7 @@ function useDoubleClick( description="クリック処理の設定オプション。" :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -28,13 +28,13 @@ function useDoubleClick( 'シングルクリックのコールバックを実行するまでの待機時間(ミリ秒単位)です。デフォルトは 250 ミリ秒です。', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: 'シングルクリック時に実行するコールバック関数。', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -61,8 +61,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('Zoom in!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/ko/useDoubleClick.md b/packages/react-simplikit/src/hooks/useDoubleClick/ko/useDoubleClick.md index 9461f253..4dcf9000 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/ko/useDoubleClick.md +++ b/packages/react-simplikit/src/hooks/useDoubleClick/ko/useDoubleClick.md @@ -19,7 +19,7 @@ function useDoubleClick( description="클릭 처리를 위한 설정 옵션이에요." :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -27,13 +27,13 @@ function useDoubleClick( '단일 클릭 콜백을 실행하기 전에 기다리는 밀리초 단위 수로, 기본값은 250ms예요.', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: '단일 클릭 시 실행되는 콜백 함수예요.', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -60,8 +60,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('확대!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return
{selected ? '선택됨' : '선택 안됨'}
; diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.md b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.md index 61b45ee6..6ea8acde 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.md +++ b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.md @@ -20,7 +20,7 @@ function useDoubleClick( description="Configuration options for click handling." :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -28,13 +28,13 @@ function useDoubleClick( 'The number of milliseconds to wait before triggering the single click callback. Defaults to 250ms.', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: 'The callback function to be executed on a single click.', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -61,8 +61,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('Zoom in!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.spec.tsx b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.spec.tsx index cf481b19..9e4c2de2 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.spec.tsx +++ b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.spec.tsx @@ -5,18 +5,18 @@ import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { useDoubleClick } from './useDoubleClick.ts'; function TestComponent({ - delay = 250, + delayMs = 250, onClick, onDoubleClick, }: { - delay?: number; + delayMs?: number; onClick?: (event: MouseEvent) => void; onDoubleClick: (event: MouseEvent) => void; }) { const handleEvent = useDoubleClick({ - delay, - click: onClick, - doubleClick: onDoubleClick, + delayMs, + onClick, + onDoubleClick, }); return ; @@ -64,7 +64,7 @@ describe('useDoubleClick', () => { expect(doubleClickSpy).toHaveBeenCalledTimes(1); }); - it('does not throw if click is not provided', () => { + it('does not throw if onClick is not provided', () => { const { getByText } = render(); const button = getByText('Test Button'); @@ -102,7 +102,7 @@ describe('useDoubleClick', () => { }); it('respects custom delay time', () => { - const { getByText } = render(); + const { getByText } = render(); const button = getByText('Test Button'); fireEvent.click(button, { detail: 1 }); @@ -116,8 +116,8 @@ describe('useDoubleClick', () => { expect(clickSpy).toHaveBeenCalledTimes(1); }); - it('does not call click if doubleClick is triggered before delay', () => { - const { getByText } = render(); + it('does not call onClick if onDoubleClick is triggered before delayMs', () => { + const { getByText } = render(); const button = getByText('Test Button'); fireEvent.click(button, { detail: 1 }); diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.ts b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.ts index 4614a9ab..765ab629 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.ts +++ b/packages/react-simplikit/src/hooks/useDoubleClick/useDoubleClick.ts @@ -3,9 +3,9 @@ import { MouseEvent, useCallback, useEffect, useRef } from 'react'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; type UseDoubleClickProps = { - delay?: number; - click?: (event: MouseEvent) => void; - doubleClick: (event: MouseEvent) => void; + delayMs?: number; + onClick?: (event: MouseEvent) => void; + onDoubleClick: (event: MouseEvent) => void; }; /** @@ -15,9 +15,9 @@ type UseDoubleClickProps = { * * @template {HTMLElement} E - The specific type of HTMLElement to be used with this hook (e.g., HTMLButtonElement, HTMLDivElement). * @param {Object} props - Configuration options for click handling. - * @param {number} [props.delay=250] - The number of milliseconds to wait before triggering the single click callback. Defaults to 250ms. - * @param {(event: MouseEvent) => void} [props.click] - The callback function to be executed on a single click. - * @param {(event: MouseEvent) => void} props.doubleClick - The callback function to be executed on a double click. Required. + * @param {number} [props.delayMs=250] - The number of milliseconds to wait before triggering the single click callback. Defaults to 250ms. + * @param {(event: MouseEvent) => void} [props.onClick] - The callback function to be executed on a single click. + * @param {(event: MouseEvent) => void} props.onDoubleClick - The callback function to be executed on a double click. Required. * * @returns {(event: MouseEvent) => void} A click handler function to attach to an element's `onClick` event. * @@ -29,8 +29,8 @@ type UseDoubleClickProps = { * const handleDoubleClick = () => alert('Zoom in!'); * * const handleEvent = useDoubleClick({ - * click: handleClick, - * doubleClick: handleDoubleClick, + * onClick: handleClick, + * onDoubleClick: handleDoubleClick, * }); * * return ( @@ -41,9 +41,9 @@ type UseDoubleClickProps = { * } */ export function useDoubleClick({ - delay = 250, - click, - doubleClick, + delayMs = 250, + onClick, + onDoubleClick, }: UseDoubleClickProps) { const clickTimeout = useRef(null); @@ -60,17 +60,17 @@ export function useDoubleClick({ (event: MouseEvent) => { clearClickTimeout(); - if (click && event.detail === 1) { + if (onClick && event.detail === 1) { clickTimeout.current = window.setTimeout(() => { - click(event); - }, delay); + onClick(event); + }, delayMs); } if (event.detail === 2) { - doubleClick(event); + onDoubleClick(event); } }, - [click, doubleClick, delay, clearClickTimeout] + [onClick, onDoubleClick, delayMs, clearClickTimeout] ); return handleEvent; diff --git a/packages/react-simplikit/src/hooks/useDoubleClick/zh-Hans/useDoubleClick.md b/packages/react-simplikit/src/hooks/useDoubleClick/zh-Hans/useDoubleClick.md index 5324ec15..82905dd9 100644 --- a/packages/react-simplikit/src/hooks/useDoubleClick/zh-Hans/useDoubleClick.md +++ b/packages/react-simplikit/src/hooks/useDoubleClick/zh-Hans/useDoubleClick.md @@ -19,7 +19,7 @@ function useDoubleClick( description="用于点击处理的配置选项。" :nested="[ { - name: 'props.delay', + name: 'props.delayMs', type: 'number', required: false, defaultValue: '250', @@ -27,13 +27,13 @@ function useDoubleClick( '触发单击回调之前等待的毫秒数。默认为 250 毫秒。', }, { - name: 'props.click', + name: 'props.onClick', type: '(event: MouseEvent) => void', required: false, description: '在单击时执行的回调函数。', }, { - name: 'props.doubleClick', + name: 'props.onDoubleClick', type: '(event: MouseEvent) => void', required: true, description: @@ -60,8 +60,8 @@ function GalleryCard() { const handleDoubleClick = () => alert('Zoom in!'); const handleEvent = useDoubleClick({ - click: handleClick, - doubleClick: handleDoubleClick, + onClick: handleClick, + onDoubleClick: handleDoubleClick, }); return ( diff --git a/packages/react-simplikit/src/hooks/useList/es/useList.md b/packages/react-simplikit/src/hooks/useList/es/useList.md index 184009e6..ca96109d 100644 --- a/packages/react-simplikit/src/hooks/useList/es/useList.md +++ b/packages/react-simplikit/src/hooks/useList/es/useList.md @@ -6,12 +6,12 @@ Proporciona una gestión eficiente del estado y funciones de acción estables. ## Interfaz ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### Parámetros - + ### Valor de retorno diff --git a/packages/react-simplikit/src/hooks/useList/ja/useList.md b/packages/react-simplikit/src/hooks/useList/ja/useList.md index 1928641e..0dde8698 100644 --- a/packages/react-simplikit/src/hooks/useList/ja/useList.md +++ b/packages/react-simplikit/src/hooks/useList/ja/useList.md @@ -6,12 +6,12 @@ ## インターフェース ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### パラメータ - + ### 戻り値 diff --git a/packages/react-simplikit/src/hooks/useList/ko/useList.md b/packages/react-simplikit/src/hooks/useList/ko/useList.md index a9de68cd..366d1d89 100644 --- a/packages/react-simplikit/src/hooks/useList/ko/useList.md +++ b/packages/react-simplikit/src/hooks/useList/ko/useList.md @@ -5,12 +5,12 @@ ## 인터페이스 ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### 파라미터 - + ### 반환 값 diff --git a/packages/react-simplikit/src/hooks/useList/useList.md b/packages/react-simplikit/src/hooks/useList/useList.md index ebb419a4..dd859ef6 100644 --- a/packages/react-simplikit/src/hooks/useList/useList.md +++ b/packages/react-simplikit/src/hooks/useList/useList.md @@ -6,12 +6,12 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### Parameters - + ### Return Value diff --git a/packages/react-simplikit/src/hooks/useList/useList.ts b/packages/react-simplikit/src/hooks/useList/useList.ts index 1b3be4ee..8af6a3fc 100644 --- a/packages/react-simplikit/src/hooks/useList/useList.ts +++ b/packages/react-simplikit/src/hooks/useList/useList.ts @@ -21,7 +21,7 @@ type UseListReturn = [ReadonlyArray, ListActions]; * * @template T - The type of the values held in the list. * - * @param {T[]} [initialState=[]] - Initial array state. + * @param {T[]} [initialValue=[]] - Initial array value. * * @returns {UseListReturn} A tuple containing the array state and actions to manipulate it. * - list `ReadonlyArray` - The current array state; @@ -55,10 +55,10 @@ type UseListReturn = [ReadonlyArray, ListActions]; * actions.reset(); * ``` */ -export function useList(initialState: T[] = []): UseListReturn { - const [list, setList] = useState(initialState); +export function useList(initialValue: T[] = []): UseListReturn { + const [list, setList] = useState(initialValue); - const preservedInitialState = usePreservedReference(initialState); + const preservedInitialValue = usePreservedReference(initialValue); const push = usePreservedCallback((value: T) => { setList(prev => [...prev, value]); @@ -93,7 +93,7 @@ export function useList(initialState: T[] = []): UseListReturn { }); const reset = usePreservedCallback(() => { - setList(preservedInitialState); + setList(preservedInitialValue); }); const actions = useMemo>( diff --git a/packages/react-simplikit/src/hooks/useList/zh-Hans/useList.md b/packages/react-simplikit/src/hooks/useList/zh-Hans/useList.md index 9e9ab241..8454bb02 100644 --- a/packages/react-simplikit/src/hooks/useList/zh-Hans/useList.md +++ b/packages/react-simplikit/src/hooks/useList/zh-Hans/useList.md @@ -5,12 +5,12 @@ ## 接口 ```ts -function useList(initialState: T[] = []): UseListReturn; +function useList(initialValue: T[] = []): UseListReturn; ``` ### 参数 - + ### 返回值 diff --git a/packages/react-simplikit/src/hooks/useMap/es/useMap.md b/packages/react-simplikit/src/hooks/useMap/es/useMap.md index 0ca9cece..dee9671a 100644 --- a/packages/react-simplikit/src/hooks/useMap/es/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/es/useMap.md @@ -6,16 +6,16 @@ Proporciona una gestión eficiente del estado y funciones de acción estables. ## Interfaz ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### Parámetros ### Valor de retorno diff --git a/packages/react-simplikit/src/hooks/useMap/ja/useMap.md b/packages/react-simplikit/src/hooks/useMap/ja/useMap.md index 15f0fe65..91939ecd 100644 --- a/packages/react-simplikit/src/hooks/useMap/ja/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/ja/useMap.md @@ -6,16 +6,16 @@ ## インターフェース ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### パラメータ ### 戻り値 diff --git a/packages/react-simplikit/src/hooks/useMap/ko/useMap.md b/packages/react-simplikit/src/hooks/useMap/ko/useMap.md index 0c0ab92e..f7ff8455 100644 --- a/packages/react-simplikit/src/hooks/useMap/ko/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/ko/useMap.md @@ -5,16 +5,16 @@ ## 인터페이스 ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### 파라미터 ### 반환 값 diff --git a/packages/react-simplikit/src/hooks/useMap/useMap.md b/packages/react-simplikit/src/hooks/useMap/useMap.md index ab50cf3d..74af5ffc 100644 --- a/packages/react-simplikit/src/hooks/useMap/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/useMap.md @@ -6,16 +6,16 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### Parameters ### Return Value diff --git a/packages/react-simplikit/src/hooks/useMap/useMap.ts b/packages/react-simplikit/src/hooks/useMap/useMap.ts index 1e60a994..b7064c6c 100644 --- a/packages/react-simplikit/src/hooks/useMap/useMap.ts +++ b/packages/react-simplikit/src/hooks/useMap/useMap.ts @@ -34,7 +34,7 @@ type UseMapReturn = [Omit, 'set' | 'clear' | 'delete'>, MapActio * * @template K - The type of the Map keys. * @template V - The type of the Map values. - * @param {MapOrEntries} initialState - Initial Map state (Map object or array of key-value pairs) + * @param {MapOrEntries} initialValue - Initial Map value (Map object or array of key-value pairs) * @returns {UseMapReturn} A tuple containing the Map state and actions to manipulate it * * @example @@ -50,12 +50,12 @@ type UseMapReturn = [Omit, 'set' | 'clear' | 'delete'>, MapActio * actions.set('user2', { name: 'Jane', age: 25 }); * ``` */ -export function useMap(initialState: MapOrEntries = new Map()): UseMapReturn { +export function useMap(initialValue: MapOrEntries = new Map()): UseMapReturn { // Initialize Map state - const [map, setMap] = useState(() => new Map(initialState)); + const [map, setMap] = useState(() => new Map(initialValue)); - // Use usePreservedReference to maintain stable reference to initialState - const preservedInitialState = usePreservedReference(initialState); + // Use usePreservedReference to maintain stable reference to initialValue + const preservedInitialValue = usePreservedReference(initialValue); const set = useCallback((key: K, value: V) => { setMap(prev => { @@ -78,8 +78,8 @@ export function useMap(initialState: MapOrEntries = new Map()): UseM }, []); const reset = useCallback(() => { - setMap(() => new Map(preservedInitialState)); - }, [preservedInitialState]); + setMap(() => new Map(preservedInitialValue)); + }, [preservedInitialValue]); const actions = useMemo>(() => { return { set, setAll, remove, reset }; diff --git a/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md b/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md index a36d74e9..984ce591 100644 --- a/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md @@ -5,16 +5,16 @@ ## 接口 ```ts -function useMap(initialState: MapOrEntries): UseMapReturn; +function useMap(initialValue: MapOrEntries): UseMapReturn; ``` ### 参数 ### 返回值 diff --git a/packages/react-simplikit/src/hooks/useSet/es/useSet.md b/packages/react-simplikit/src/hooks/useSet/es/useSet.md index b8514301..d95a33f6 100644 --- a/packages/react-simplikit/src/hooks/useSet/es/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/es/useSet.md @@ -6,15 +6,15 @@ Proporciona una gestión eficiente del estado y funciones de acción estables. ## Interfaz ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### Parámetros ### Valor de retorno diff --git a/packages/react-simplikit/src/hooks/useSet/ja/useSet.md b/packages/react-simplikit/src/hooks/useSet/ja/useSet.md index 6c2f3e92..b2526daa 100644 --- a/packages/react-simplikit/src/hooks/useSet/ja/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/ja/useSet.md @@ -6,15 +6,15 @@ Set を状態として管理する React フックです。 ## インターフェース ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### パラメータ ### 戻り値 diff --git a/packages/react-simplikit/src/hooks/useSet/ko/useSet.md b/packages/react-simplikit/src/hooks/useSet/ko/useSet.md index 8bb45683..f631b76f 100644 --- a/packages/react-simplikit/src/hooks/useSet/ko/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/ko/useSet.md @@ -5,15 +5,15 @@ ## 인터페이스 ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### 파라미터 ### 반환 값 diff --git a/packages/react-simplikit/src/hooks/useSet/useSet.md b/packages/react-simplikit/src/hooks/useSet/useSet.md index 3701fbba..f9745fac 100644 --- a/packages/react-simplikit/src/hooks/useSet/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/useSet.md @@ -6,15 +6,15 @@ Provides efficient state management and stable action functions. ## Interface ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### Parameters ### Return Value diff --git a/packages/react-simplikit/src/hooks/useSet/useSet.ts b/packages/react-simplikit/src/hooks/useSet/useSet.ts index 03c8dc7a..d242b42a 100644 --- a/packages/react-simplikit/src/hooks/useSet/useSet.ts +++ b/packages/react-simplikit/src/hooks/useSet/useSet.ts @@ -22,7 +22,7 @@ type UseSetReturn = [Omit, 'add' | 'clear' | 'delete'>, SetActions] * * @template T - The type of the values held in the set. * - * @param {SetOrValues} [initialState=new Set()] - Initial Set state (Set object or array of values). + * @param {SetOrValues} [initialValue=new Set()] - Initial Set value (Set object or array of values). * * @returns {UseSetReturn} A tuple containing the Set state and actions to manipulate it. * - [0] `Omit, 'add' | 'clear' | 'delete'>` - The current Set state with mutation methods hidden; @@ -51,10 +51,10 @@ type UseSetReturn = [Omit, 'add' | 'clear' | 'delete'>, SetActions] * } * ``` */ -export function useSet(initialState: SetOrValues = new Set()): UseSetReturn { - const [set, setSet] = useState(() => new Set(initialState)); +export function useSet(initialValue: SetOrValues = new Set()): UseSetReturn { + const [set, setSet] = useState(() => new Set(initialValue)); - const preservedInitialState = usePreservedReference(initialState); + const preservedInitialValue = usePreservedReference(initialValue); const add = usePreservedCallback((value: T) => { setSet(prev => { @@ -89,7 +89,7 @@ export function useSet(initialState: SetOrValues = new Set()): UseSetRetur }); const reset = usePreservedCallback(() => { - setSet(() => new Set(preservedInitialState)); + setSet(() => new Set(preservedInitialValue)); }); const actions = useMemo>( diff --git a/packages/react-simplikit/src/hooks/useSet/zh-Hans/useSet.md b/packages/react-simplikit/src/hooks/useSet/zh-Hans/useSet.md index 8516ea38..f528f7c5 100644 --- a/packages/react-simplikit/src/hooks/useSet/zh-Hans/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/zh-Hans/useSet.md @@ -5,15 +5,15 @@ ## 接口 ```ts -function useSet(initialState: SetOrValues = new Set()): UseSetReturn; +function useSet(initialValue: SetOrValues = new Set()): UseSetReturn; ``` ### 参数 ### 返回值 From 917b09bcb46ba11df420a6a292a0379f0d149987 Mon Sep 17 00:00:00 2001 From: hyesung oh Date: Thu, 1 Oct 2026 15:59:59 +0900 Subject: [PATCH 02/10] refactor: rename equality options to equalityFn (#544) --- .changeset/equality-fn-options.md | 5 +++++ .../references/usePreservedReference.md | 4 ++-- .../react-simplikit/references/usePrevious.md | 13 ++++++----- .../es/usePreservedReference.md | 4 ++-- .../ja/usePreservedReference.md | 4 ++-- .../ko/usePreservedReference.md | 4 ++-- .../usePreservedReference.md | 4 ++-- .../usePreservedReference.spec.ts | 10 ++++----- .../usePreservedReference.ts | 8 +++---- .../zh-Hans/usePreservedReference.md | 4 ++-- .../src/hooks/usePrevious/es/usePrevious.md | 13 ++++++----- .../src/hooks/usePrevious/ja/usePrevious.md | 13 ++++++----- .../src/hooks/usePrevious/ko/usePrevious.md | 11 ++++++---- .../src/hooks/usePrevious/usePrevious.md | 13 ++++++----- .../src/hooks/usePrevious/usePrevious.spec.ts | 22 +++++++++++++++++++ .../src/hooks/usePrevious/usePrevious.ts | 12 +++++----- .../hooks/usePrevious/zh-Hans/usePrevious.md | 11 ++++++---- 17 files changed, 99 insertions(+), 56 deletions(-) create mode 100644 .changeset/equality-fn-options.md diff --git a/.changeset/equality-fn-options.md b/.changeset/equality-fn-options.md new file mode 100644 index 00000000..32bed967 --- /dev/null +++ b/.changeset/equality-fn-options.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +Rename the comparison parameter of `usePrevious` and `usePreservedReference` to `equalityFn`, matching `useControlledState`. `usePrevious` now compares with `Object.is` by default, so rendering `NaN` again no longer overwrites the previous value, and a change between `0` and `-0` is detected. diff --git a/packages/plugin/skills/react-simplikit/references/usePreservedReference.md b/packages/plugin/skills/react-simplikit/references/usePreservedReference.md index ebdca232..1195b1a9 100644 --- a/packages/plugin/skills/react-simplikit/references/usePreservedReference.md +++ b/packages/plugin/skills/react-simplikit/references/usePreservedReference.md @@ -9,7 +9,7 @@ It prevents unnecessary re-renders while always allowing access to the latest da ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -23,7 +23,7 @@ function usePreservedReference( /> diff --git a/packages/plugin/skills/react-simplikit/references/usePrevious.md b/packages/plugin/skills/react-simplikit/references/usePrevious.md index d2d0d35e..f821ff6c 100644 --- a/packages/plugin/skills/react-simplikit/references/usePrevious.md +++ b/packages/plugin/skills/react-simplikit/references/usePrevious.md @@ -2,13 +2,16 @@ `usePrevious` is a React hook that returns the previous value of the input state. It preserves the previous value unchanged when re-renders occur without state changes. -If the state is an object or requires custom change detection, a `compare` function can be provided. -By default, state changes are detected using `prev === next`. +If the state is an object or requires custom change detection, an `equalityFn` can be provided. +By default, state changes are detected using `Object.is(prev, next)`. ## Interface ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### Parameters @@ -21,9 +24,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### Return Value diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/es/usePreservedReference.md b/packages/react-simplikit/src/hooks/usePreservedReference/es/usePreservedReference.md index 2336d5f0..305c8301 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/es/usePreservedReference.md +++ b/packages/react-simplikit/src/hooks/usePreservedReference/es/usePreservedReference.md @@ -9,7 +9,7 @@ Evita renderizados innecesarios y te permite acceder siempre a los datos más re ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -23,7 +23,7 @@ function usePreservedReference( /> diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/ja/usePreservedReference.md b/packages/react-simplikit/src/hooks/usePreservedReference/ja/usePreservedReference.md index 8fb06f21..b87fc91d 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/ja/usePreservedReference.md +++ b/packages/react-simplikit/src/hooks/usePreservedReference/ja/usePreservedReference.md @@ -8,7 +8,7 @@ ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -22,7 +22,7 @@ function usePreservedReference( /> diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/ko/usePreservedReference.md b/packages/react-simplikit/src/hooks/usePreservedReference/ko/usePreservedReference.md index fe5a8dae..bc67332b 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/ko/usePreservedReference.md +++ b/packages/react-simplikit/src/hooks/usePreservedReference/ko/usePreservedReference.md @@ -7,7 +7,7 @@ ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -21,7 +21,7 @@ function usePreservedReference( /> diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.md b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.md index ebdca232..1195b1a9 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.md +++ b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.md @@ -9,7 +9,7 @@ It prevents unnecessary re-renders while always allowing access to the latest da ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -23,7 +23,7 @@ function usePreservedReference( /> diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.spec.ts b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.spec.ts index 1d494d6b..14abf47f 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.spec.ts +++ b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.spec.ts @@ -48,11 +48,11 @@ describe('usePreservedReference', () => { const initialValue = { key: 'value', action: initialFunction }; const initialProps: { value: any; - areValuesEqual?: (a: any, b: any) => boolean; - } = { value: initialValue, areValuesEqual: areDeeplyEqual }; + equalityFn?: (a: any, b: any) => boolean; + } = { value: initialValue, equalityFn: areDeeplyEqual }; const { result, rerender } = await renderHookSSR( - ({ value, areValuesEqual }) => usePreservedReference(value, areValuesEqual), + ({ value, equalityFn }) => usePreservedReference(value, equalityFn), { initialProps, } @@ -65,11 +65,11 @@ describe('usePreservedReference', () => { expect(result.current.action).toBe(initialFunction); /** - * The default behavior of areValuesEqual provided by usePreservedReference + * The default behavior of equalityFn provided by usePreservedReference * is to compare using JSON.stringify, which cannot detect changes in functions. * Therefore, a separate comparator that can compare functions should be provided. */ - rerender({ value: newValue, areValuesEqual: areDeeplyEqual }); + rerender({ value: newValue, equalityFn: areDeeplyEqual }); expect(result.current.action).not.toBe(initialFunction); expect(result.current.action).toBe(newFunction); }); diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.ts b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.ts index f28165ce..661d9a13 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.ts +++ b/packages/react-simplikit/src/hooks/usePreservedReference/usePreservedReference.ts @@ -19,7 +19,7 @@ type NotNullishValue = {}; * * @param {T} value - The value to maintain the reference for. It returns a new reference * if the state value changes after comparison. - * @param {(a: T, b: T) => boolean} [areValuesEqual] - An optional function to determine + * @param {(a: T, b: T) => boolean} [equalityFn] - An optional function to determine * if two values are equal. By default, it uses `JSON.stringify` for comparison. * * @returns {T} Returns the same reference if the value is considered equal to the previous one, @@ -51,7 +51,7 @@ type NotNullishValue = {}; */ export function usePreservedReference( value: T, - areValuesEqual: (a: T, b: T) => boolean = areDeeplyEqual + equalityFn: (a: T, b: T) => boolean = areDeeplyEqual ): T { // Without `'use no memo'`, React Compiler throws when `panicThreshold` is not `'none'` // because this hook intentionally reads and updates refs during render, inside `useMemo`. @@ -60,11 +60,11 @@ export function usePreservedReference( const ref = useRef(value); return useMemo(() => { - if (!areValuesEqual(ref.current, value)) { + if (!equalityFn(ref.current, value)) { ref.current = value; } return ref.current; - }, [areValuesEqual, value]); + }, [equalityFn, value]); } function areDeeplyEqual(x: T, y: T) { diff --git a/packages/react-simplikit/src/hooks/usePreservedReference/zh-Hans/usePreservedReference.md b/packages/react-simplikit/src/hooks/usePreservedReference/zh-Hans/usePreservedReference.md index 34a9f0f5..4d6617b8 100644 --- a/packages/react-simplikit/src/hooks/usePreservedReference/zh-Hans/usePreservedReference.md +++ b/packages/react-simplikit/src/hooks/usePreservedReference/zh-Hans/usePreservedReference.md @@ -7,7 +7,7 @@ ```ts function usePreservedReference( value: T, - areValuesEqual?: (a: T, b: T) => boolean + equalityFn?: (a: T, b: T) => boolean ): T; ``` @@ -21,7 +21,7 @@ function usePreservedReference( /> diff --git a/packages/react-simplikit/src/hooks/usePrevious/es/usePrevious.md b/packages/react-simplikit/src/hooks/usePrevious/es/usePrevious.md index 3efc0f88..8a849ba0 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/es/usePrevious.md +++ b/packages/react-simplikit/src/hooks/usePrevious/es/usePrevious.md @@ -2,13 +2,16 @@ `usePrevious` es un Hook de React que devuelve el valor anterior del estado que le pasas. Conserva el valor anterior sin cambios cuando hay nuevos renderizados sin cambios en el estado. -Si el estado es un objeto o necesitas personalizar la detección de cambios, puedes proporcionar una función `compare`. -De forma predeterminada, el Hook detecta los cambios de estado mediante `prev === next`. +Si el estado es un objeto o necesitas personalizar la detección de cambios, puedes proporcionar una función `equalityFn`. +De forma predeterminada, el Hook detecta los cambios de estado mediante `Object.is(prev, next)`. ## Interfaz ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### Parámetros @@ -21,9 +24,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### Valor de retorno diff --git a/packages/react-simplikit/src/hooks/usePrevious/ja/usePrevious.md b/packages/react-simplikit/src/hooks/usePrevious/ja/usePrevious.md index 8a756f84..0f638613 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/ja/usePrevious.md +++ b/packages/react-simplikit/src/hooks/usePrevious/ja/usePrevious.md @@ -2,13 +2,16 @@ `usePrevious` は、渡された状態の前の値を返す React フックです。 状態が変わらずに再レンダリングされた場合、前の値をそのまま保持します。 -状態がオブジェクトの場合や、変更の検出方法をカスタマイズしたい場合は、`compare` 関数を指定できます。 -デフォルトでは、`prev === next` を使って状態の変化を検出します。 +状態がオブジェクトの場合や、変更の検出方法をカスタマイズしたい場合は、`equalityFn` 関数を指定できます。 +デフォルトでは、`Object.is(prev, next)` を使って状態の変化を検出します。 ## インターフェース ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### パラメータ @@ -21,9 +24,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### 戻り値 diff --git a/packages/react-simplikit/src/hooks/usePrevious/ko/usePrevious.md b/packages/react-simplikit/src/hooks/usePrevious/ko/usePrevious.md index 4d0288fa..6cddba87 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/ko/usePrevious.md +++ b/packages/react-simplikit/src/hooks/usePrevious/ko/usePrevious.md @@ -1,11 +1,14 @@ # usePrevious -입력 상태의 이전 값을 반환해요. 만약 다시 렌더링이 발생하지만 상태 값이 변경되지 않으면 이전 값은 변경되지 않아요. 상태가 객체이거나 사용자 정의 변경 감지가 필요한 경우, `compare` 함수를 제공할 수 있어요. 기본적으로 상태 변경은 `prev === next`을 사용하여 감지해요. +입력 상태의 이전 값을 반환해요. 만약 다시 렌더링이 발생하지만 상태 값이 변경되지 않으면 이전 값은 변경되지 않아요. 상태가 객체이거나 사용자 정의 변경 감지가 필요한 경우, `equalityFn` 함수를 제공할 수 있어요. 기본적으로 상태 변경은 `Object.is(prev, next)`를 사용하여 감지해요. ## 인터페이스 ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### 파라미터 @@ -18,9 +21,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### 반환 값 diff --git a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.md b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.md index d2d0d35e..f821ff6c 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.md +++ b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.md @@ -2,13 +2,16 @@ `usePrevious` is a React hook that returns the previous value of the input state. It preserves the previous value unchanged when re-renders occur without state changes. -If the state is an object or requires custom change detection, a `compare` function can be provided. -By default, state changes are detected using `prev === next`. +If the state is an object or requires custom change detection, an `equalityFn` can be provided. +By default, state changes are detected using `Object.is(prev, next)`. ## Interface ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### Parameters @@ -21,9 +24,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### Return Value diff --git a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.spec.ts b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.spec.ts index 0f6d2685..01f8fd05 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.spec.ts +++ b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.spec.ts @@ -45,6 +45,28 @@ describe('usePrevious', () => { expect(result.current).toBe(2); }); + it('should keep the previous state when NaN is rendered again', async () => { + const { result, rerender } = await renderHookSSR(({ state }) => usePrevious(state), { + initialProps: { state: 1 }, + }); + + rerender({ state: NaN }); + expect(result.current).toBe(1); + + rerender({ state: NaN }); + expect(result.current).toBe(1); + }); + + it('should treat 0 and -0 as different states', async () => { + const { result, rerender } = await renderHookSSR(({ state }) => usePrevious(state), { + initialProps: { state: 0 }, + }); + + rerender({ state: -0 }); + rerender({ state: 1 }); + expect(result.current).toBe(-0); + }); + it('should custom compare function works', async () => { const compareObject = (prev: Record | undefined, next: Record) => { if (prev === undefined) { diff --git a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.ts b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.ts index 7de27ea3..32665c48 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/usePrevious.ts +++ b/packages/react-simplikit/src/hooks/usePrevious/usePrevious.ts @@ -6,18 +6,16 @@ of the previous-value contract is ever added to this file. */ import { useRef } from 'react'; -const strictEquals = (prev: T, next: T) => prev === next; - /** * @description * `usePrevious` is a React hook that returns the previous value of the input state. * It preserves the previous value unchanged when re-renders occur without state changes. - * If the state is an object or requires custom change detection, a `compare` function can be provided. - * By default, state changes are detected using `prev === next`. + * If the state is an object or requires custom change detection, an `equalityFn` can be provided. + * By default, state changes are detected using `Object.is(prev, next)`. * * @template T - The type of the state. * @param {T} state - The state whose previous value is to be tracked. - * @param {(prev: T, next: T) => boolean} [compare] - An optional comparison function to determine if the state has changed. + * @param {(prev: T, next: T) => boolean} [equalityFn] - An optional function to determine if two states are equal. By default, it uses `Object.is` for comparison. * * @returns {T} The previous value of the state. * @@ -26,7 +24,7 @@ const strictEquals = (prev: T, next: T) => prev === next; * // initial value of previousCount is `0` * const previousCount = usePrevious(count); */ -export function usePrevious(state: T, compare: (prev: T, next: T) => boolean = strictEquals): T { +export function usePrevious(state: T, equalityFn: (prev: T, next: T) => boolean = Object.is): T { // Without `'use no memo'`, React Compiler throws when `panicThreshold` is not `'none'` // because this hook intentionally reads and updates refs during render. 'use no memo'; @@ -40,7 +38,7 @@ export function usePrevious(state: T, compare: (prev: T, next: T) => boolean return prevRef.current; } - if (!compare(currentRef.current, state)) { + if (!equalityFn(currentRef.current, state)) { prevRef.current = currentRef.current; currentRef.current = state; } diff --git a/packages/react-simplikit/src/hooks/usePrevious/zh-Hans/usePrevious.md b/packages/react-simplikit/src/hooks/usePrevious/zh-Hans/usePrevious.md index c7532b11..89ca1b59 100644 --- a/packages/react-simplikit/src/hooks/usePrevious/zh-Hans/usePrevious.md +++ b/packages/react-simplikit/src/hooks/usePrevious/zh-Hans/usePrevious.md @@ -1,11 +1,14 @@ # usePrevious -`usePrevious` 是一个返回输入状态之前值的 React Hook。当发生未伴随状态变化的重复渲染时,它会原样保留之前的值。如果该状态是对象或需要自定义变化检测,则可以提供 `compare` 函数。默认情况下,状态变化通过 `prev === next` 来检测。 +`usePrevious` 是一个返回输入状态之前值的 React Hook。当发生未伴随状态变化的重复渲染时,它会原样保留之前的值。如果该状态是对象或需要自定义变化检测,则可以提供 `equalityFn` 函数。默认情况下,状态变化通过 `Object.is(prev, next)` 来检测。 ## 接口 ```ts -function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; +function usePrevious( + state: T, + equalityFn?: (prev: T, next: T) => boolean +): T; ``` ### 参数 @@ -18,9 +21,9 @@ function usePrevious(state: T, compare?: (prev: T, next: T) => boolean): T; /> ### 返回值 From 15aea8a81b788353889c9a941ef5f545cc5f07e8 Mon Sep 17 00:00:00 2001 From: SiHyunLee Date: Tue, 6 Oct 2026 15:02:04 +0900 Subject: [PATCH 03/10] refactor(useThrottle, useThrottledCallback): replace edges with leading and trailing (#548) Co-authored-by: hyesung oh --- .changeset/petite-beans-fetch.md | 5 +++ .../react-simplikit/references/useThrottle.md | 22 ++++++---- .../references/useThrottledCallback.md | 16 ++++++-- .../src/hooks/useThrottle/es/useThrottle.md | 22 ++++++---- .../src/hooks/useThrottle/ja/useThrottle.md | 22 ++++++---- .../src/hooks/useThrottle/ko/useThrottle.md | 22 ++++++---- .../src/hooks/useThrottle/useThrottle.md | 22 ++++++---- .../src/hooks/useThrottle/useThrottle.spec.ts | 40 ++++++++++++++++++- .../src/hooks/useThrottle/useThrottle.ts | 33 +++++++++++---- .../hooks/useThrottle/zh-Hans/useThrottle.md | 22 ++++++---- .../es/useThrottledCallback.md | 16 ++++++-- .../ja/useThrottledCallback.md | 16 ++++++-- .../ko/useThrottledCallback.md | 16 ++++++-- .../useThrottledCallback.md | 16 ++++++-- .../useThrottledCallback.spec.ts | 16 +++++--- .../useThrottledCallback.ts | 11 +++-- .../zh-Hans/useThrottledCallback.md | 16 ++++++-- .../useThrottledValue/useThrottledValue.ts | 18 +-------- 18 files changed, 251 insertions(+), 100 deletions(-) create mode 100644 .changeset/petite-beans-fetch.md diff --git a/.changeset/petite-beans-fetch.md b/.changeset/petite-beans-fetch.md new file mode 100644 index 00000000..4c42c88d --- /dev/null +++ b/.changeset/petite-beans-fetch.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': major +--- + +Replace the `edges` option in `useThrottle` and `useThrottledCallback` with optional `leading` and `trailing` booleans. Both default to `true`, preserving the behavior when options are omitted. diff --git a/packages/plugin/skills/react-simplikit/references/useThrottle.md b/packages/plugin/skills/react-simplikit/references/useThrottle.md index a4068503..94063491 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottle.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottle.md @@ -10,7 +10,7 @@ such as when handling scroll or resize events. function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -32,16 +32,24 @@ function useThrottle any>( @@ -62,7 +70,7 @@ const throttledScroll = useThrottle( console.log('Scroll event'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md b/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md index de3fefc4..6e0e02b4 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md @@ -31,12 +31,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: 'The number of milliseconds to throttle invocations to.', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - 'An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.', + 'If true, allows an immediate call at the start of a throttle window.', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + 'If true, allows a pending call to run after the delay with the latest value.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md index 951cf042..42a6ddd4 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md @@ -10,7 +10,7 @@ por ejemplo, al manejar eventos de desplazamiento o de cambio de tamaño. function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -32,16 +32,24 @@ function useThrottle any>( @@ -62,7 +70,7 @@ const throttledScroll = useThrottle( console.log('Evento de desplazamiento'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md index f1598fb5..bdfd73d9 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md @@ -9,7 +9,7 @@ function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -31,16 +31,24 @@ function useThrottle any>( @@ -61,7 +69,7 @@ const throttledScroll = useThrottle( console.log('Scroll event'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md index 09cabf2d..9b508722 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md @@ -8,7 +8,7 @@ function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -30,16 +30,24 @@ function useThrottle any>( @@ -60,7 +68,7 @@ const throttledScroll = useThrottle( console.log('Scroll event'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md index a4068503..94063491 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md @@ -10,7 +10,7 @@ such as when handling scroll or resize events. function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -32,16 +32,24 @@ function useThrottle any>( @@ -62,7 +70,7 @@ const throttledScroll = useThrottle( console.log('Scroll event'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.spec.ts b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.spec.ts index bd95ac11..e3165801 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.spec.ts +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.spec.ts @@ -89,7 +89,7 @@ describe('useThrottle', () => { const callback = vi.fn(); const throttleMs = 50; - const { result } = await renderHookSSR(() => useThrottle(callback, throttleMs, { edges: ['leading', 'trailing'] })); + const { result } = await renderHookSSR(() => useThrottle(callback, throttleMs, { leading: true, trailing: true })); result.current(); @@ -114,4 +114,42 @@ describe('useThrottle', () => { vi.useRealTimers(); }); + + it('should skip the leading edge when leading is false', async () => { + vi.useFakeTimers(); + + const callback = vi.fn(); + const throttleMs = 50; + const { result } = await renderHookSSR(() => useThrottle(callback, throttleMs, { leading: false })); + + result.current(); + + expect(callback).toHaveBeenCalledTimes(0); + + await vi.advanceTimersByTimeAsync(throttleMs); + + expect(callback).toHaveBeenCalledTimes(1); + + vi.useRealTimers(); + }); + + it('should skip the trailing edge when trailing is false', async () => { + vi.useFakeTimers(); + + const callback = vi.fn(); + const throttleMs = 50; + const { result } = await renderHookSSR(() => useThrottle(callback, throttleMs, { trailing: false })); + + result.current(); + + expect(callback).toHaveBeenCalledTimes(1); + + result.current(); + + await vi.advanceTimersByTimeAsync(throttleMs); + + expect(callback).toHaveBeenCalledTimes(1); + + vi.useRealTimers(); + }); }); diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts index 6d600ecb..9a904054 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts @@ -2,10 +2,14 @@ import { useEffect, useMemo } from 'react'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; -import { usePreservedReference } from '../usePreservedReference/index.ts'; import { throttle } from './throttle.ts'; +type ThrottleOptions = { + leading?: boolean; + trailing?: boolean; +}; + /** * @description * `useThrottle` is a React hook that creates a throttled version of a callback function. @@ -15,14 +19,15 @@ import { throttle } from './throttle.ts'; * @template {(...args: any[]) => any} F - The type of the callback function. * @param {F} callback - The function to be throttled. * @param {number} wait - The number of milliseconds to throttle invocations to. - * @param {{ edges?: Array<'leading' | 'trailing'> }} [options] - Options to control the behavior of the throttle. - * @param {Array<'leading' | 'trailing'>} [options.edges=['leading', 'trailing']] - An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both. + * @param {ThrottleOptions} [options] - Options to control the behavior of the throttle. + * @param {boolean} [options.leading=true] - If `true`, allows an immediate call at the start of a throttle window. + * @param {boolean} [options.trailing=true] - If `true`, allows a pending call to run after the delay with the latest arguments. * @returns {F & { cancel: () => void }} - Returns the throttled function with a `cancel` method to cancel pending executions. * * @example * const throttledScroll = useThrottle(() => { * console.log('Scroll event'); - * }, 200, { edges: ['leading', 'trailing'] }); + * }, 200, { leading: true, trailing: true }); * * useEffect(() => { * window.addEventListener('scroll', throttledScroll); @@ -35,14 +40,26 @@ import { throttle } from './throttle.ts'; export function useThrottle any>( callback: F, wait: number, - options?: Parameters[2] + { leading = true, trailing = true }: ThrottleOptions = {} ) { const preservedCallback = usePreservedCallback(callback); - const preservedOptions = usePreservedReference(options ?? {}); + + const edges = useMemo(() => { + const _edges: Array<'leading' | 'trailing'> = []; + if (leading) { + _edges.push('leading'); + } + + if (trailing) { + _edges.push('trailing'); + } + + return _edges; + }, [leading, trailing]); const throttledCallback = useMemo( - () => throttle(preservedCallback, wait, preservedOptions), - [preservedOptions, preservedCallback, wait] + () => throttle(preservedCallback, wait, { edges }), + [preservedCallback, wait, edges] ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md index 3689d4c2..26c80710 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md @@ -8,7 +8,7 @@ function useThrottle any>( callback: F, wait: number, - options?: { edges?: Array<'leading' | 'trailing'> } + options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -30,16 +30,24 @@ function useThrottle any>( @@ -60,7 +68,7 @@ const throttledScroll = useThrottle( console.log('Scroll event'); }, 200, - { edges: ['leading', 'trailing'] } + { leading: true, trailing: true } ); useEffect(() => { diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/es/useThrottledCallback.md b/packages/react-simplikit/src/hooks/useThrottledCallback/es/useThrottledCallback.md index 654faea1..88ac8f4a 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/es/useThrottledCallback.md +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/es/useThrottledCallback.md @@ -31,12 +31,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: 'El intervalo en milisegundos que limita la frecuencia de las llamadas.', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - 'Un arreglo opcional que especifica si la función debe ejecutarse al inicio del intervalo, al final o en ambos momentos.', + 'Si es true, permite una llamada inmediata al inicio del intervalo de limitación de frecuencia.', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + 'Si es true, permite ejecutar una llamada pendiente después de la espera con el valor más reciente.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/ja/useThrottledCallback.md b/packages/react-simplikit/src/hooks/useThrottledCallback/ja/useThrottledCallback.md index d2a7433b..c7ac4b09 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/ja/useThrottledCallback.md +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/ja/useThrottledCallback.md @@ -31,12 +31,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: '呼び出しを制限する間隔(ミリ秒単位)。', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - '関数を区間の開始時、終了時、またはその両方で呼び出すかを指定する省略可能な配列。', + 'true の場合、スロットリング区間の開始時に即座に呼び出せます。', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + 'true の場合、遅延時間の経過後に、待機中の呼び出しを最新の値で実行できます。', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/ko/useThrottledCallback.md b/packages/react-simplikit/src/hooks/useThrottledCallback/ko/useThrottledCallback.md index c9c5c1ce..36ad9b34 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/ko/useThrottledCallback.md +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/ko/useThrottledCallback.md @@ -30,12 +30,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: '호출을 스로틀링할 밀리초(ms)이에요.', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - '함수가 시작점, 끝점 또는 둘 다에서 호출될지 여부를 지정하는 선택적 배열이에요.', + 'true이면 스로틀 구간이 시작될 때 즉시 호출할 수 있어요.', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + 'true이면 대기 중인 호출을 지연 시간이 지난 뒤 최신 값으로 실행할 수 있어요.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.md b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.md index de3fefc4..6e0e02b4 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.md +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.md @@ -31,12 +31,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: 'The number of milliseconds to throttle invocations to.', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - 'An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.', + 'If true, allows an immediate call at the start of a throttle window.', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + 'If true, allows a pending call to run after the delay with the latest value.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.spec.ts b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.spec.ts index 509ea87e..aee15950 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.spec.ts +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.spec.ts @@ -47,7 +47,9 @@ describe('useThrottledCallback', () => { it('should handle trailing edge', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useThrottledCallback({ onChange, timeThreshold: 100, edges: ['trailing'] })); + const { result } = renderHookSSR(() => + useThrottledCallback({ onChange, timeThreshold: 100, leading: false, trailing: true }) + ); result.current(true); expect(onChange).not.toBeCalled(); @@ -73,7 +75,7 @@ describe('useThrottledCallback', () => { it('should cleanup on unmount', async () => { const onChange = vi.fn(); const { result, unmount } = await renderHookSSR(() => - useThrottledCallback({ onChange, timeThreshold: 100, edges: ['trailing'] }) + useThrottledCallback({ onChange, timeThreshold: 100, leading: false, trailing: true }) ); result.current(true); @@ -86,7 +88,7 @@ describe('useThrottledCallback', () => { it('should handle leading and trailing edges together', () => { const onChange = vi.fn(); const { result } = renderHookSSR(() => - useThrottledCallback({ onChange, timeThreshold: 100, edges: ['leading', 'trailing'] }) + useThrottledCallback({ onChange, timeThreshold: 100, leading: true, trailing: true }) ); result.current(true); @@ -132,7 +134,9 @@ describe('useThrottledCallback', () => { it('discards a pending trailing value when the caller returns to the last forwarded one', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useThrottledCallback({ onChange, timeThreshold: 100, edges: ['trailing'] })); + const { result } = renderHookSSR(() => + useThrottledCallback({ onChange, timeThreshold: 100, leading: false, trailing: true }) + ); result.current('seoul'); vi.advanceTimersByTime(100); @@ -177,7 +181,9 @@ describe('useThrottledCallback', () => { it('keeps forwarding once per interval while values keep changing with trailing edge only', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useThrottledCallback({ onChange, timeThreshold: 100, edges: ['trailing'] })); + const { result } = renderHookSSR(() => + useThrottledCallback({ onChange, timeThreshold: 100, leading: false, trailing: true }) + ); for (let i = 1; i <= 10; i++) { result.current(i); diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.ts b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.ts index 1184fb12..c0ddf579 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.ts +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/useThrottledCallback.ts @@ -3,7 +3,8 @@ import { useRef } from 'react'; import { useThrottle } from '../useThrottle/index.ts'; type ThrottleOptions = { - edges?: Array<'leading' | 'trailing'>; + leading?: boolean; + trailing?: boolean; }; /** @@ -24,7 +25,8 @@ const NOT_INVOKED = Symbol('NOT_INVOKED'); * @param {Object} options - The options object. * @param {(newValue: T) => void} options.onChange - The callback to throttle. A call with the same value as the last forwarded one is skipped. * @param {number} options.timeThreshold - The number of milliseconds to throttle invocations to. - * @param {Array<'leading' | 'trailing'>} [options.edges=['leading', 'trailing']] - An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both. + * @param {boolean} [options.leading=true] - If `true`, allows an immediate call at the start of a throttle window. + * @param {boolean} [options.trailing=true] - If `true`, allows a pending call to run after the delay with the latest value. * * @returns {(nextValue: T) => void} A throttled function that forwards the value to `onChange` at most once per interval. * @@ -46,7 +48,8 @@ const NOT_INVOKED = Symbol('NOT_INVOKED'); export function useThrottledCallback({ onChange, timeThreshold, - edges = ['leading', 'trailing'], + leading = true, + trailing = true, }: ThrottleOptions & { onChange: (newValue: T) => void; timeThreshold: number; @@ -64,6 +67,6 @@ export function useThrottledCallback({ lastForwardedRef.current = nextValue; }, timeThreshold, - { edges } + { leading, trailing } ); } diff --git a/packages/react-simplikit/src/hooks/useThrottledCallback/zh-Hans/useThrottledCallback.md b/packages/react-simplikit/src/hooks/useThrottledCallback/zh-Hans/useThrottledCallback.md index af3800b8..108dac42 100644 --- a/packages/react-simplikit/src/hooks/useThrottledCallback/zh-Hans/useThrottledCallback.md +++ b/packages/react-simplikit/src/hooks/useThrottledCallback/zh-Hans/useThrottledCallback.md @@ -30,12 +30,20 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; description: '将调用节流到的毫秒数。', }, { - name: 'options.edges', - type: 'Array<\'leading\' | \'trailing\'>', + name: 'options.leading', + type: 'boolean', required: false, - defaultValue: '[\'leading\', \'trailing\']', + defaultValue: 'true', description: - '一个可选的数组,用于指定应在领先边缘、落后边缘或两者处调用该函数。', + '如果为 true,则允许在节流时间窗口开始时立即调用。', + }, + { + name: 'options.trailing', + type: 'boolean', + required: false, + defaultValue: 'true', + description: + '如果为 true,则允许待执行的调用在延迟结束后以最新值运行。', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useThrottledValue/useThrottledValue.ts b/packages/react-simplikit/src/hooks/useThrottledValue/useThrottledValue.ts index 4800f34b..25de14a6 100644 --- a/packages/react-simplikit/src/hooks/useThrottledValue/useThrottledValue.ts +++ b/packages/react-simplikit/src/hooks/useThrottledValue/useThrottledValue.ts @@ -7,8 +7,6 @@ type ThrottleOptions = { trailing?: boolean; }; -type Edge = 'leading' | 'trailing'; - /** * @description * `useThrottledValue` is a React hook that returns a throttled copy of the given value. @@ -55,7 +53,8 @@ export function useThrottledValue( ): T { const [throttledValue, setThrottledValue] = useState(() => value); const throttled = useThrottle((next: T) => setThrottledValue(() => next), wait, { - edges: toEdges(leading, trailing), + leading, + trailing, }); const lastForwardedRef = useRef(value); @@ -75,16 +74,3 @@ export function useThrottledValue( return throttledValue; } - -// Adapts this hook's `leading` / `trailing` booleans to the `edges` array `useThrottle` still takes. -// Delete once `useThrottle` accepts the booleans, and pass the options straight through. -function toEdges(leading: boolean, trailing: boolean): Edge[] { - const edges: Edge[] = []; - if (leading) { - edges.push('leading'); - } - if (trailing) { - edges.push('trailing'); - } - return edges; -} From 9d16479f53552bc1d4acad4af26ef1a2c9484a45 Mon Sep 17 00:00:00 2001 From: hyesung oh Date: Wed, 7 Oct 2026 18:08:56 +0900 Subject: [PATCH 04/10] feat(useCallbackOnce): rename useCallbackOncePerRender to useCallbackOnce (#545) --- .changeset/callback-once.md | 5 + .scripts/commands/generateDocs/index.spec.ts | 38 ++++++ .scripts/commands/generateDocs/index.ts | 115 +++++++++--------- .../commands/generateSkill/catalog.spec.ts | 78 +++++++++++- .scripts/commands/generateSkill/catalog.ts | 62 +++++++++- .scripts/commands/generateSkill/index.spec.ts | 82 ++++++++++++- .scripts/commands/generateSkill/index.ts | 40 ++++-- .scripts/utils/assertLlmsOutput.ts | 18 ++- .../utils/collectDeprecatedExports.spec.ts | 46 +++++++ .scripts/utils/collectDeprecatedExports.ts | 30 +++++ .scripts/utils/jsdoc.spec.ts | 69 +++++++++++ .scripts/utils/jsdoc.ts | 52 ++++++++ .scripts/verifyDocs.ts | 28 +++++ .scripts/verifyDocsI18n.ts | 30 ++++- .scripts/verifySkill.ts | 41 ++++--- .vitepress/libs/legacyRedirects.mts | 9 +- .../plugin/skills/react-simplikit/SKILL.md | 10 +- .../references/useCallbackOnce.md | 67 ++++++++++ .../references/useCallbackOncePerRender.md | 66 ---------- .../useCallbackOnce/es/useCallbackOnce.md | 67 ++++++++++ .../index.ts | 1 + .../ja/useCallbackOnce.md} | 27 ++-- .../useCallbackOnce/ko/useCallbackOnce.md | 65 ++++++++++ .../hooks/useCallbackOnce/useCallbackOnce.md | 67 ++++++++++ .../useCallbackOnce.test.ts} | 22 ++-- .../useCallbackOnce.ts} | 19 +-- .../useCallbackOncePerRender.ts | 9 ++ .../zh-Hans/useCallbackOnce.md | 65 ++++++++++ .../es/useCallbackOncePerRender.md | 66 ---------- .../ko/useCallbackOncePerRender.md | 65 ---------- .../useCallbackOncePerRender.md | 66 ---------- .../zh-Hans/useCallbackOncePerRender.md | 65 ---------- packages/react-simplikit/src/index.ts | 2 +- 33 files changed, 1028 insertions(+), 464 deletions(-) create mode 100644 .changeset/callback-once.md create mode 100644 .scripts/utils/collectDeprecatedExports.spec.ts create mode 100644 .scripts/utils/collectDeprecatedExports.ts create mode 100644 .scripts/utils/jsdoc.spec.ts create mode 100644 .scripts/utils/jsdoc.ts create mode 100644 packages/plugin/skills/react-simplikit/references/useCallbackOnce.md delete mode 100644 packages/plugin/skills/react-simplikit/references/useCallbackOncePerRender.md create mode 100644 packages/react-simplikit/src/hooks/useCallbackOnce/es/useCallbackOnce.md rename packages/react-simplikit/src/hooks/{useCallbackOncePerRender => useCallbackOnce}/index.ts (56%) rename packages/react-simplikit/src/hooks/{useCallbackOncePerRender/ja/useCallbackOncePerRender.md => useCallbackOnce/ja/useCallbackOnce.md} (50%) create mode 100644 packages/react-simplikit/src/hooks/useCallbackOnce/ko/useCallbackOnce.md create mode 100644 packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.md rename packages/react-simplikit/src/hooks/{useCallbackOncePerRender/useCallbackOncePerRender.spec.ts => useCallbackOnce/useCallbackOnce.test.ts} (73%) rename packages/react-simplikit/src/hooks/{useCallbackOncePerRender/useCallbackOncePerRender.ts => useCallbackOnce/useCallbackOnce.ts} (60%) create mode 100644 packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOncePerRender.ts create mode 100644 packages/react-simplikit/src/hooks/useCallbackOnce/zh-Hans/useCallbackOnce.md delete mode 100644 packages/react-simplikit/src/hooks/useCallbackOncePerRender/es/useCallbackOncePerRender.md delete mode 100644 packages/react-simplikit/src/hooks/useCallbackOncePerRender/ko/useCallbackOncePerRender.md delete mode 100644 packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.md delete mode 100644 packages/react-simplikit/src/hooks/useCallbackOncePerRender/zh-Hans/useCallbackOncePerRender.md diff --git a/.changeset/callback-once.md b/.changeset/callback-once.md new file mode 100644 index 00000000..bae13d4d --- /dev/null +++ b/.changeset/callback-once.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': minor +--- + +Add `useCallbackOnce`, which runs a callback only once until its `deps` change and has the same signature and behavior as `useCallbackOncePerRender`. `useCallbackOncePerRender` is now deprecated in favor of `useCallbackOnce` and still works as an alias of it. The second example now lists `userId` in the effect's dependencies, because the returned function keeps the same reference and an effect keyed only on it does not run again when `deps` change. diff --git a/.scripts/commands/generateDocs/index.spec.ts b/.scripts/commands/generateDocs/index.spec.ts index 75cdcf4c..f0d2b5fc 100644 --- a/.scripts/commands/generateDocs/index.spec.ts +++ b/.scripts/commands/generateDocs/index.spec.ts @@ -677,4 +677,42 @@ export function useNoReturnValue() {}` expect(document).toContain('\nThis function does not return anything.'); }); + + it('refuses an export whose JSDoc is @deprecated, because it has no page', async () => { + await expect( + render( + 'useOld', + `/** + * @description + * \`useOld\` is the name \`useNew\` had before. + * + * @deprecated Use \`useNew\` instead. + */ +export const useOld = useNew;` + ) + ).rejects.toThrow('useOld is @deprecated, and a deprecated export has no documentation page'); + }); + + it('still renders a page when only an earlier comment, such as an options type, is @deprecated', async () => { + const document = await render( + 'useCurrent', + `type Options = { + /** @deprecated Use \`delay\` instead. */ + wait?: number; +}; + +/** + * @description + * \`useCurrent\` does something. + * + * @returns {void} + * + * @example + * useCurrent(); + */ +export function useCurrent(options: Options) {}` + ); + + expect(document.startsWith('# useCurrent\n\n`useCurrent` does something.\n')).toBe(true); + }); }); diff --git a/.scripts/commands/generateDocs/index.ts b/.scripts/commands/generateDocs/index.ts index 4d80822f..fd456000 100644 --- a/.scripts/commands/generateDocs/index.ts +++ b/.scripts/commands/generateDocs/index.ts @@ -1,4 +1,4 @@ -import { parse, Spec as OriginSpec, tokenizers } from 'comment-parser'; +import { parse, Spec as OriginSpec } from 'comment-parser'; import glob from 'fast-glob'; import * as fs from 'fs/promises'; import { Listr } from 'listr2'; @@ -6,6 +6,7 @@ import path from 'path'; import * as prettier from 'prettier'; import { getRootPath } from '../../utils/getRootPath.ts'; +import { parseOptions, readDeprecation, trimBlock } from '../../utils/jsdoc.ts'; import { generateSkill } from '../generateSkill/index.ts'; type Spec = Pick; @@ -23,10 +24,17 @@ const prettierConfig: prettier.Options = { * * `verifyDocs.ts` compares a committed page against this, so the formatting applied here has to be * the same formatting the page is written with — hence prettier runs inside, not at the call site. + * + * Throws for an export whose JSDoc is `@deprecated`: it has no page, and the page of the export + * that replaces it says what it was renamed from. */ export async function renderEnglishDoc(name: string, sourceFilePath: string): Promise { const documentPath = `${path.dirname(sourceFilePath)}/${name}.md`; - const docSource = await jsdocToMd(name, parseJSDoc(await fs.readFile(sourceFilePath, 'utf-8'))); + const source = await fs.readFile(sourceFilePath, 'utf-8'); + + assertHasPage(name, source); + + const docSource = await jsdocToMd(name, parseJSDoc(source)); return prettier.format(docSource, { ...(await prettier.resolveConfig(documentPath)), @@ -36,43 +44,50 @@ export async function renderEnglishDoc(name: string, sourceFilePath: string): Pr export async function generateDocs(names: string[]) { const tasks = new Listr([], { concurrent: 10 }); + const targets = names.map(name => [name, glob.sync(`**/${name}.ts*`, { cwd: getRootPath() })[0]]); + + // Checked before any task runs: the tasks below collect their errors instead of exiting, and a + // deprecated name has to fail the command without writing anything. + for (const [name, sourceFilePath] of targets) { + if (sourceFilePath !== undefined) { + assertHasPage(name, await fs.readFile(sourceFilePath, 'utf-8')); + } + } - names - .map(name => [name, glob.sync(`**/${name}.ts*`, { cwd: getRootPath() })[0]]) - .forEach(([name, sourceFilePath]) => { - const subCtx: { document?: string } = {}; - tasks.add([ - { - title: `Generate documents: ${sourceFilePath}`, - task: async (_, task) => - task.newListr<{ document?: string }>( - [ - { - title: `Convert JSDoc to markdown`, - task: async ctx => { - ctx.document = await renderEnglishDoc(name, sourceFilePath); - }, + targets.forEach(([name, sourceFilePath]) => { + const subCtx: { document?: string } = {}; + tasks.add([ + { + title: `Generate documents: ${sourceFilePath}`, + task: async (_, task) => + task.newListr<{ document?: string }>( + [ + { + title: `Convert JSDoc to markdown`, + task: async ctx => { + ctx.document = await renderEnglishDoc(name, sourceFilePath); }, - { - title: `Write English document`, - task: async ctx => { - const { document } = ctx; - - if (document != null) { - // Written already formatted: `.prettierignore`'s `src/hooks/**/*.md` is anchored to the - // repo root and never reaches packages/, so prettier (yarn fix, autofix.ci) reformats - // these pages later. `generateSkill()` below copies them, and an unformatted copy - // would drift from the page the moment prettier runs. - await fs.writeFile(`${path.dirname(sourceFilePath)}/${name}.md`, document); - } - }, + }, + { + title: `Write English document`, + task: async ctx => { + const { document } = ctx; + + if (document != null) { + // Written already formatted: `.prettierignore`'s `src/hooks/**/*.md` is anchored to the + // repo root and never reaches packages/, so prettier (yarn fix, autofix.ci) reformats + // these pages later. `generateSkill()` below copies them, and an unformatted copy + // would drift from the page the moment prettier runs. + await fs.writeFile(`${path.dirname(sourceFilePath)}/${name}.md`, document); + } }, - ], - { concurrent: false, ctx: subCtx, exitOnError: false } - ), - }, - ]); - }); + }, + ], + { concurrent: false, ctx: subCtx, exitOnError: false } + ), + }, + ]); + }); await tasks.run(); @@ -80,15 +95,13 @@ export async function generateDocs(names: string[]) { await generateSkill(); } -// These tags carry no name, but the stock name tokenizer still takes the description's first word -// as one ("An object…" became "object…"). Skipping it for them keeps the sentence whole. -const NAMELESS_TAGS = new Set(['returns', 'remarks', 'description']); -const skipNameForNamelessTags = (spec: OriginSpec) => (NAMELESS_TAGS.has(spec.tag) ? spec : tokenizers.name()(spec)); - -const parseOptions = (spacing: 'compact' | 'preserve') => ({ - spacing, - tokenizers: [tokenizers.tag(), tokenizers.type(spacing), skipNameForNamelessTags, tokenizers.description(spacing)], -}); +function assertHasPage(name: string, source: string) { + if (readDeprecation(source) != null) { + throw new Error( + `${name} is @deprecated, and a deprecated export has no documentation page — document the rename on its replacement's page instead` + ); + } +} function parseJSDoc(source: string) { const parsedComments = parse(source, parseOptions('compact')); @@ -284,18 +297,6 @@ ${remarks.length === 0 ? '' : `\n## Notes\n\n${remarks}\n`}`; const LIST_ITEM = /^[-*]\s/; -/** - * `@description` and `@remarks` are Markdown and go to the page as written: a line break inside a - * paragraph renders as a space, and fences, numbered and nested lists keep their meaning. - */ -function trimBlock(text: string) { - return text - .split('\n') - .map(line => line.trimEnd()) - .join('\n') - .trim(); -} - /** * The `@returns` intro lands in an HTML attribute where a line break becomes `
`, so the * source's wrapping is joined back into one line; a blank line still separates paragraphs. diff --git a/.scripts/commands/generateSkill/catalog.spec.ts b/.scripts/commands/generateSkill/catalog.spec.ts index af4cb54d..ac980a31 100644 --- a/.scripts/commands/generateSkill/catalog.spec.ts +++ b/.scripts/commands/generateSkill/catalog.spec.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'vitest'; -import { extractDescription, getCategory, renderSkill } from './catalog.ts'; +import { extractDescription, getCategory, readCatalogNames, readDeprecatedNames, renderSkill } from './catalog.ts'; describe('getCategory', () => { it('derives the category from the export source path', () => { @@ -60,6 +60,7 @@ describe('renderSkill', () => { { name: 'isIOS', category: 'utils', description: 'Detects iOS.' }, { name: 'useToggle', category: 'hooks', description: 'Toggles a | boolean.' }, ], + deprecatedEntries: [], }); assert.equal( @@ -80,12 +81,85 @@ describe('renderSkill', () => { | --- | --- | | [\`isIOS\`](references/isIOS.md) | Detects iOS. | +## Learn more +` + ); + }); + + it('lists a deprecated entry under Deprecated as an unlinked row with its notice on one line', () => { + const rendered = renderSkill({ + template: '# Skill\n\n## Catalog\n\n\n\n## Learn more\n', + entries: [{ name: 'useNew', category: 'hooks', description: 'Does it.' }], + deprecatedEntries: [{ name: 'useOld', notice: 'Use `useNew` | not this.\nIt goes away\nlater.' }], + }); + + assert.equal( + rendered, + `# Skill + +## Catalog + +### hooks + +| Name | Description | +| --- | --- | +| [\`useNew\`](references/useNew.md) | Does it. | + +### Deprecated + +These still work but are kept only for backward compatibility. Do not use them in new code; each row names the replacement. + +| Name | Notice | +| --- | --- | +| \`useOld\` | Use \`useNew\` \\| not this. It goes away later. | + ## Learn more ` ); }); it('rejects a template without the placeholder', () => { - assert.throws(() => renderSkill({ template: '# Skill\n', entries: [] }), //); + assert.throws(() => renderSkill({ template: '# Skill\n', entries: [], deprecatedEntries: [] }), //); + }); +}); + +describe('readCatalogNames', () => { + it('reads the linked rows only, so a deprecated export is not counted as documented', () => { + const skill = renderSkill({ + template: '# Skill\n\n| Need | Use |\n| --- | --- |\n| Toggle | `useToggle` |\n\n\n', + entries: [ + { name: 'isIOS', category: 'utils', description: 'Detects iOS.' }, + { name: 'useNew', category: 'hooks', description: 'Does it.' }, + ], + deprecatedEntries: [{ name: 'useOld', notice: 'Use `useNew` instead.' }], + }); + + assert.deepEqual(readCatalogNames(skill), ['useNew', 'isIOS']); + }); +}); + +describe('readDeprecatedNames', () => { + it('reads only the names in the Deprecated table of the rendered catalog', () => { + const skill = renderSkill({ + template: + '# Skill\n\n| Need | Use |\n| --- | --- |\n| `useBefore` | Toggle |\n\n\n\n## Learn more\n\n| Name | Link |\n| --- | --- |\n| `useAfter` | Docs |\n', + entries: [ + { name: 'isIOS', category: 'utils', description: 'Detects iOS.' }, + { name: 'useNew', category: 'hooks', description: 'Does it.' }, + ], + deprecatedEntries: [{ name: 'useOld', notice: 'Use `useNew` instead.' }], + }); + + assert.deepEqual(readDeprecatedNames(skill), ['useOld']); + }); + + it('reports no deprecated names when the catalog has no Deprecated table', () => { + const skill = renderSkill({ + template: '\n', + entries: [{ name: 'useNew', category: 'hooks', description: 'Does it.' }], + deprecatedEntries: [], + }); + + assert.deepEqual(readDeprecatedNames(skill), []); }); }); diff --git a/.scripts/commands/generateSkill/catalog.ts b/.scripts/commands/generateSkill/catalog.ts index 16930698..07ef78db 100644 --- a/.scripts/commands/generateSkill/catalog.ts +++ b/.scripts/commands/generateSkill/catalog.ts @@ -8,13 +8,26 @@ export type CatalogEntry = { description: string; }; +/** A deprecated export: it has no documentation page, so it is listed by name with its `@deprecated` text. */ +export type DeprecatedEntry = { + name: string; + notice: string; +}; + type RenderSkillOptions = { template: string; entries: CatalogEntry[]; + deprecatedEntries: DeprecatedEntry[]; }; const CATALOG_PLACEHOLDER = ''; +const DEPRECATED_HEADING = '### Deprecated'; + +const CATALOG_ROW = /^\| \[`([^`]+)`\]\(references\/\1\.md\) \| .+ \|$/gm; + +const DEPRECATED_ROW = /^\| `([^`]+)` \| .+ \|$/gm; + /** Catalog heading for an export: the first segment of its source path, `./hooks/x/index.ts` → `hooks`. */ export function getCategory(sourcePath: string): Category { const category = sourcePath.replace(/^\.\//, '').split('/')[0]; @@ -66,10 +79,12 @@ export function extractDescription(markdown: string, name: string): string { } /** - * Fills the template's `` with one table per category. Categories keep the - * order of `CATEGORIES`; rows keep the order they are given (sorted by name upstream). + * Fills the template's `` with one table per category, then a Deprecated table + * when there are deprecated entries. Categories keep the order of `CATEGORIES`; rows keep the order + * they are given (sorted by name upstream). A Deprecated row links nowhere, because a deprecated + * export has no reference page. */ -export function renderSkill({ template, entries }: RenderSkillOptions): string { +export function renderSkill({ template, entries, deprecatedEntries }: RenderSkillOptions): string { if (!template.includes(CATALOG_PLACEHOLDER)) { throw new Error(`The skill template must contain ${CATALOG_PLACEHOLDER}`); } @@ -93,7 +108,46 @@ export function renderSkill({ template, entries }: RenderSkillOptions): string { ]; }); - return template.replace(CATALOG_PLACEHOLDER, sections.join('\n').trimEnd()); + const deprecatedSection = + deprecatedEntries.length === 0 + ? [] + : [ + DEPRECATED_HEADING, + '', + 'These still work but are kept only for backward compatibility. Do not use them in new code; each row names the replacement.', + '', + '| Name | Notice |', + '| --- | --- |', + // A wrapped `@deprecated` text is joined into one line so it fits the table cell. + ...deprecatedEntries.map( + ({ name, notice }) => `| \`${name}\` | ${escapeTableCell(notice.replace(/\s*\n\s*/g, ' '))} |` + ), + '', + ]; + + return template.replace(CATALOG_PLACEHOLDER, [...sections, ...deprecatedSection].join('\n').trimEnd()); +} + +/** Names of the catalog rows of a rendered `SKILL.md`: the rows that link `references/.md` from their first cell. */ +export function readCatalogNames(skill: string): string[] { + return [...skill.matchAll(CATALOG_ROW)].map(match => match[1]); +} + +/** + * Names in the Deprecated table of a rendered `SKILL.md`, which `renderSkill` writes last in the + * catalog; empty when there is none. The section ends at the next heading, so a table further + * down the template is not read as part of it. + */ +export function readDeprecatedNames(skill: string): string[] { + const deprecatedStart = skill.indexOf(`\n${DEPRECATED_HEADING}\n`); + + if (deprecatedStart === -1) { + return []; + } + + const [section] = skill.slice(deprecatedStart + `\n${DEPRECATED_HEADING}\n`.length).split(/^#+ /m); + + return [...section.matchAll(DEPRECATED_ROW)].map(match => match[1]); } function escapeTableCell(text: string): string { diff --git a/.scripts/commands/generateSkill/index.spec.ts b/.scripts/commands/generateSkill/index.spec.ts index bcd25135..20c8e1d5 100644 --- a/.scripts/commands/generateSkill/index.spec.ts +++ b/.scripts/commands/generateSkill/index.spec.ts @@ -43,6 +43,7 @@ export { useToggle } from './hooks/useToggle/index.ts'; true ); assert.equal(skill.includes(''), false); + assert.equal(skill.includes('### Deprecated'), false); assert.equal( await fs.readFile(path.join(outputDirectory, 'references', 'useToggle.md'), 'utf8'), '# useToggle\n\n`useToggle` flips a boolean. More text.\n\n## Interface\n' @@ -78,10 +79,71 @@ export { useToggle } from './hooks/useToggle/index.ts'; assert.equal((await fs.readFile(path.join(outputDirectory, 'SKILL.md'), 'utf8')).includes('isIOS'), false); }); + it('lists an export whose JSDoc is @deprecated under Deprecated, without a page or a reference', async () => { + const root = await writeFixtureRoot({ + index: ` +export { useNew, useOld } from './hooks/useNew/index.ts'; +`, + pages: { 'hooks/useNew/useNew.md': '# useNew\n\n`useNew` flips a boolean.\n' }, + sources: { + 'hooks/useNew/useNew.ts': 'export function useNew() {}\n', + 'hooks/useNew/useOld.ts': `/** + * @description + * \`useOld\` is the name \`useNew\` had before. + * + * @deprecated Use \`useNew\` instead. + */ +export const useOld = useNew; +`, + }, + }); + const outputDirectory = path.join(root, 'out'); + + await generateSkill({ root, outputDirectory }); + + const skill = await fs.readFile(path.join(outputDirectory, 'SKILL.md'), 'utf8'); + assert.equal( + skill.includes( + '### hooks\n\n| Name | Description |\n| --- | --- |\n| [`useNew`](references/useNew.md) | `useNew` flips a boolean. |\n\n### Deprecated\n' + ), + true + ); + assert.equal(skill.includes('| Name | Notice |\n| --- | --- |\n| `useOld` | Use `useNew` instead. |'), true); + assert.equal(skill.includes('references/useOld.md'), false); + assert.deepEqual(await fs.readdir(path.join(outputDirectory, 'references')), ['useNew.md']); + }); + + it('reads a component source from a .tsx file', async () => { + const root = await writeFixtureRoot({ + index: `export { Separated } from './components/Separated/index.ts';\n`, + pages: { 'components/Separated/Separated.md': '# Separated\n\n`Separated` joins children.\n' }, + sources: { 'components/Separated/Separated.tsx': 'export function Separated() {}\n' }, + }); + const outputDirectory = path.join(root, 'out'); + + await generateSkill({ root, outputDirectory }); + + assert.deepEqual(await fs.readdir(path.join(outputDirectory, 'references')), ['Separated.md']); + }); + + it('fails when an export has no source file next to its index', async () => { + const root = await writeFixtureRoot({ + index: `export { useToggle } from './hooks/useToggle/index.ts';\n`, + pages: { 'hooks/useToggle/useToggle.md': '# useToggle\n\n`useToggle` flips a boolean.\n' }, + sources: {}, + }); + + await assert.rejects( + generateSkill({ root, outputDirectory: path.join(root, 'out') }), + /useToggle has no source file/ + ); + }); + it('fails when an export has no documentation page', async () => { const root = await writeFixtureRoot({ index: `export { useToggle } from './hooks/useToggle/index.ts';\n`, pages: {}, + sources: { 'hooks/useToggle/useToggle.ts': 'export function useToggle() {}\n' }, }); await assert.rejects( @@ -91,18 +153,28 @@ export { useToggle } from './hooks/useToggle/index.ts'; }); }); -async function writeFixtureRoot({ index, pages }: { index: string; pages: Record }): Promise { +type FixtureRoot = { + index: string; + pages: Record; + /** Replaces the default: a `.ts` without JSDoc next to each page, which is how a current export looks. */ + sources?: Record; +}; + +async function writeFixtureRoot({ index, pages, sources }: FixtureRoot): Promise { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'react-simplikit-skill-')); fixtureDirectories.push(root); const sourceDirectory = path.join(root, path.dirname(PACKAGE_INDEX_FILE)); + const pageSources = Object.fromEntries( + Object.keys(pages).map(pagePath => [pagePath.replace(/\.md$/, '.ts'), 'export {};\n']) + ); await fs.mkdir(sourceDirectory, { recursive: true }); await fs.writeFile(path.join(root, PACKAGE_INDEX_FILE), index); - for (const [relativePath, content] of Object.entries(pages)) { - const pagePath = path.join(sourceDirectory, relativePath); - await fs.mkdir(path.dirname(pagePath), { recursive: true }); - await fs.writeFile(pagePath, content); + for (const [relativePath, content] of Object.entries({ ...pages, ...(sources ?? pageSources) })) { + const filePath = path.join(sourceDirectory, relativePath); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content); } return root; diff --git a/.scripts/commands/generateSkill/index.ts b/.scripts/commands/generateSkill/index.ts index 222c8a82..8128fe58 100644 --- a/.scripts/commands/generateSkill/index.ts +++ b/.scripts/commands/generateSkill/index.ts @@ -3,8 +3,9 @@ import path from 'node:path'; import { collectPublicExportEntries } from '../../utils/collectPublicExports.ts'; import { getRootPath } from '../../utils/getRootPath.ts'; +import { readDeprecation } from '../../utils/jsdoc.ts'; -import { CatalogEntry, extractDescription, getCategory, renderSkill } from './catalog.ts'; +import { CatalogEntry, DeprecatedEntry, extractDescription, getCategory, renderSkill } from './catalog.ts'; export const SKILL_DIRECTORY = 'packages/plugin/skills/react-simplikit'; export const PACKAGE_INDEX_FILE = 'packages/react-simplikit/src/index.ts'; @@ -19,7 +20,11 @@ type GenerateSkillOptions = { /** * Writes the consumer-facing agent skill: `SKILL.md` (template + a catalog of every public export) * and `references/.md` (a verbatim copy of each export's English documentation page). - * The output depends only on `index.ts` and the pages, so re-running on unchanged sources changes nothing. + * The output depends only on `index.ts`, the pages and each export's `@deprecated` tag, so re-running + * on unchanged sources changes nothing. + * + * An export whose JSDoc is `@deprecated` has no page, so it is recognised from its source file. It + * gets no reference page and is listed in the Deprecated table instead of its category. */ export async function generateSkill({ root = getRootPath(), @@ -33,20 +38,41 @@ export async function generateSkill({ await fs.rm(referencesDirectory, { force: true, recursive: true }); await fs.mkdir(referencesDirectory, { recursive: true }); - const entries: CatalogEntry[] = await Promise.all( + const publicExports: Array<{ entry: CatalogEntry } | { deprecatedEntry: DeprecatedEntry }> = await Promise.all( (await collectPublicExportEntries(indexFilePath)).map(async ({ name, sourcePath }) => { - const documentPath = path.join(packageSourceDirectory, path.dirname(sourcePath), `${name}.md`); - const markdown = await readDocumentationPage(documentPath, name); + const exportDirectory = path.join(packageSourceDirectory, path.dirname(sourcePath)); + const notice = readDeprecation(await readExportSource(exportDirectory, name)); + + if (notice != null) { + return { deprecatedEntry: { name, notice } }; + } + + const markdown = await readDocumentationPage(path.join(exportDirectory, `${name}.md`), name); await fs.writeFile(path.join(referencesDirectory, `${name}.md`), markdown); - return { name, category: getCategory(sourcePath), description: extractDescription(markdown, name) }; + return { entry: { name, category: getCategory(sourcePath), description: extractDescription(markdown, name) } }; }) ); + const entries = publicExports.flatMap(item => ('entry' in item ? [item.entry] : [])); + const deprecatedEntries = publicExports.flatMap(item => ('deprecatedEntry' in item ? [item.deprecatedEntry] : [])); const template = await fs.readFile(TEMPLATE_FILE, 'utf8'); - await fs.writeFile(path.join(outputDirectory, 'SKILL.md'), renderSkill({ template, entries })); + await fs.writeFile(path.join(outputDirectory, 'SKILL.md'), renderSkill({ template, entries, deprecatedEntries })); +} + +/** The file an export is declared in sits next to the `index.ts` that re-exports it, named after the export. */ +async function readExportSource(exportDirectory: string, name: string): Promise { + for (const extension of ['ts', 'tsx']) { + try { + return await fs.readFile(path.join(exportDirectory, `${name}.${extension}`), 'utf8'); + } catch { + continue; + } + } + + throw new Error(`${name} has no source file at ${path.join(exportDirectory, name)}.ts(x)`); } async function readDocumentationPage(documentPath: string, name: string): Promise { diff --git a/.scripts/utils/assertLlmsOutput.ts b/.scripts/utils/assertLlmsOutput.ts index 3d3d6fb4..5e25a0a7 100644 --- a/.scripts/utils/assertLlmsOutput.ts +++ b/.scripts/utils/assertLlmsOutput.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import * as fs from 'node:fs/promises'; import path from 'node:path'; +import { collectDeprecatedExports } from './collectDeprecatedExports.ts'; import { collectPublicExports } from './collectPublicExports.ts'; type AssertLlmsOutputOptions = { @@ -18,8 +19,8 @@ const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:(?:hooks|compo /** * Checks the llms outputs vitepress-plugin-llms wrote into a docs build: - * every public export has a page in llms.txt, localized copies are not listed, - * and the per-page Markdown exists. + * every public export that is not deprecated has a page in llms.txt, localized copies are not + * listed, and the per-page Markdown exists. */ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlmsOutputOptions): Promise { const llmsTxt = await fs.readFile(path.join(buildOutputDirectory, 'llms.txt'), 'utf8'); @@ -35,11 +36,18 @@ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlm ); } - for (const name of await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE))) { + const publicExports = await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE)); + const deprecatedExports = new Set(await collectDeprecatedExports(root, publicExports)); + + // A deprecated export has no page. Its old URL only serves a copy of its replacement's Markdown, + // which must stay out of the listing or an agent would read the same page under two names. + for (const name of publicExports) { assert.equal( links.some(link => link.endsWith(`/${name}.md`)), - true, - `llms.txt must link the documentation page of ${name}` + !deprecatedExports.has(name), + deprecatedExports.has(name) + ? `llms.txt must not link a page for ${name}, which is @deprecated` + : `llms.txt must link the documentation page of ${name}` ); } diff --git a/.scripts/utils/collectDeprecatedExports.spec.ts b/.scripts/utils/collectDeprecatedExports.spec.ts new file mode 100644 index 00000000..66f468e1 --- /dev/null +++ b/.scripts/utils/collectDeprecatedExports.spec.ts @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, it } from 'vitest'; + +import { collectDeprecatedExports } from './collectDeprecatedExports.ts'; + +const fixtureDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(fixtureDirectories.splice(0).map(directory => fs.rm(directory, { force: true, recursive: true }))); +}); + +describe('collectDeprecatedExports', () => { + it('keeps the exports whose own JSDoc is @deprecated, wherever their source sits', async () => { + const root = await writeFixtureRoot({ + 'hooks/useNew/useNew.ts': '/**\n * @description Does it.\n */\nexport function useNew() {}\n', + 'hooks/useNew/useNew.test.ts': '/**\n * @deprecated Use `nothing` instead.\n */\n', + 'hooks/useNew/useOld.ts': + '/**\n * @description The old name.\n * @deprecated Use `useNew` instead.\n */\nexport const useOld = useNew;\n', + 'components/Old/Old.tsx': '/**\n * @deprecated Use `New` instead.\n */\nexport function Old() {}\n', + }); + + assert.deepEqual(await collectDeprecatedExports(root, ['Old', 'useNew', 'useOld']), ['Old', 'useOld']); + }); + + it('fails on an export that has no source file', async () => { + const root = await writeFixtureRoot({}); + + await assert.rejects(collectDeprecatedExports(root, ['useGone']), /useGone is exported but has no source file/); + }); +}); + +async function writeFixtureRoot(sources: Record): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'react-simplikit-deprecated-')); + fixtureDirectories.push(root); + + for (const [relativePath, content] of Object.entries(sources)) { + const filePath = path.join(root, 'packages/react-simplikit/src', relativePath); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content); + } + + return root; +} diff --git a/.scripts/utils/collectDeprecatedExports.ts b/.scripts/utils/collectDeprecatedExports.ts new file mode 100644 index 00000000..ad10c247 --- /dev/null +++ b/.scripts/utils/collectDeprecatedExports.ts @@ -0,0 +1,30 @@ +import glob from 'fast-glob'; +import assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; + +import { readDeprecation } from './jsdoc.ts'; + +/** + * The given public exports whose own JSDoc is `@deprecated`, in the order given. Such an export has + * no documentation page, so every check that expects one page per export leaves these out. + * + * Each source is found by name anywhere under `src/`, not through the path `generateSkill` derives + * from `index.ts`, so the verify scripts do not repeat a mistake the generator makes. + */ +export async function collectDeprecatedExports(root: string, publicExports: string[]): Promise { + const deprecations = await Promise.all( + publicExports.map(async name => { + const [sourceFilePath] = await glob(`packages/react-simplikit/src/**/${name}.ts?(x)`, { + absolute: true, + cwd: root, + ignore: ['**/*.spec.*', '**/*.test.*'], + }); + + assert.ok(sourceFilePath, `${name} is exported but has no source file`); + + return readDeprecation(await fs.readFile(sourceFilePath, 'utf8')); + }) + ); + + return publicExports.filter((_, index) => deprecations[index] != null); +} diff --git a/.scripts/utils/jsdoc.spec.ts b/.scripts/utils/jsdoc.spec.ts new file mode 100644 index 00000000..bf2c76c4 --- /dev/null +++ b/.scripts/utils/jsdoc.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; + +import { readDeprecation } from './jsdoc.ts'; + +describe('readDeprecation', () => { + it('reads @deprecated from the last comment in the file, which documents the export', () => { + expect( + readDeprecation(`/** + * @description Does something. + * @deprecated Use \`useNew\` instead. + */ +export function useOld() {}`) + ).toBe('Use `useNew` instead.'); + }); + + it('ignores @deprecated on an earlier comment, such as an options type', () => { + expect( + readDeprecation(`type Options = { + /** @deprecated Use \`delay\` instead. */ + wait?: number; +}; + +/** + * @description Does something. + */ +export function useCurrent(options: Options) {}`) + ).toBeUndefined(); + }); + + it('throws on a @deprecated that does not say what to use instead', () => { + expect(() => + readDeprecation(`/** + * @description Does something. + * @deprecated + */ +export function useOld() {}`) + ).toThrow('@deprecated must name the replacement in backticks'); + }); + + it('throws on a @deprecated that names no replacement', () => { + expect(() => + readDeprecation(`/** + * @description Does something. + * @deprecated Will be removed in v2. + */ +export function useOld() {}`) + ).toThrow('@deprecated must name the replacement in backticks'); + }); + + it('throws on a @deprecated that opens with {@link}, which the parser reads as a type', () => { + expect(() => + readDeprecation(`/** + * @description Does something. + * @deprecated {@link useNew} instead. + */ +export function useOld() {}`) + ).toThrow('@deprecated must name the replacement in backticks'); + }); + + it('throws on a @deprecated that names the replacement with {@link}, which the skill catalog would show as written', () => { + expect(() => + readDeprecation(`/** + * @description Does something. + * @deprecated Use {@link useNew} instead. + */ +export function useOld() {}`) + ).toThrow('@deprecated must name the replacement in backticks'); + }); +}); diff --git a/.scripts/utils/jsdoc.ts b/.scripts/utils/jsdoc.ts new file mode 100644 index 00000000..afbc0454 --- /dev/null +++ b/.scripts/utils/jsdoc.ts @@ -0,0 +1,52 @@ +import { parse, Spec, tokenizers } from 'comment-parser'; + +// These tags carry no name, but the stock name tokenizer still takes the description's first word +// as one ("An object…" became "object…"). Skipping it for them keeps the sentence whole. +const NAMELESS_TAGS = new Set(['returns', 'remarks', 'description', 'deprecated']); +const skipNameForNamelessTags = (spec: Spec) => (NAMELESS_TAGS.has(spec.tag) ? spec : tokenizers.name()(spec)); + +export const parseOptions = (spacing: 'compact' | 'preserve') => ({ + spacing, + tokenizers: [tokenizers.tag(), tokenizers.type(spacing), skipNameForNamelessTags, tokenizers.description(spacing)], +}); + +/** + * `@description` and `@remarks` are Markdown and go to the page as written: a line break inside a + * paragraph renders as a space, and fences, numbered and nested lists keep their meaning. + */ +export function trimBlock(text: string) { + return text + .split('\n') + .map(line => line.trimEnd()) + .join('\n') + .trim(); +} + +/** + * The `@deprecated` text of an export's own comment (the last one in its source file), or + * `undefined` when there is none. A `@deprecated` on another comment in the file, such as an + * options type, does not mark the export itself. + * + * A deprecated export has no documentation page, so this is the only place the docs and skill + * tooling learn about it from. + */ +export function readDeprecation(source: string): string | undefined { + const tag = parse(source, parseOptions('preserve')) + .at(-1) + ?.tags.find(tag => tag.tag === 'deprecated'); + + if (tag == null) { + return undefined; + } + + const text = trimBlock(tag.description); + const namesReplacement = /`[^`]+`/.test(text); + + if (!namesReplacement) { + throw new Error( + '@deprecated must name the replacement in backticks, e.g. "@deprecated Use `newName` instead.", not with {@link newName}' + ); + } + + return text; +} diff --git a/.scripts/verifyDocs.ts b/.scripts/verifyDocs.ts index 2b09b736..96d229b3 100644 --- a/.scripts/verifyDocs.ts +++ b/.scripts/verifyDocs.ts @@ -5,14 +5,17 @@ import path from 'node:path'; import { renderEnglishDoc } from './commands/generateDocs/index.ts'; import { PACKAGE_INDEX_FILE } from './commands/generateSkill/index.ts'; +import { collectDeprecatedExports } from './utils/collectDeprecatedExports.ts'; import { collectPublicExports } from './utils/collectPublicExports.ts'; import { getRootPath } from './utils/getRootPath.ts'; const root = getRootPath(); const publicExports = await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE)); +const deprecatedExports = new Set(await collectDeprecatedExports(root, publicExports)); // The English pages are generated from JSDoc, and `verifySkill.ts` checks the skill against those // pages — so a page that drifts from its source carries the drift into the skill unnoticed. +// A deprecated export is the exception: it has no page, and its replacement's page names it. for (const name of publicExports) { const [sourceFilePath] = await glob(`packages/react-simplikit/src/**/${name}.ts?(x)`, { absolute: true, @@ -24,6 +27,22 @@ for (const name of publicExports) { const documentPath = path.join(path.dirname(sourceFilePath), `${name}.md`); + if (deprecatedExports.has(name)) { + // The sidebar, the reference index and the legacy redirects list one entry per folder, so an + // alias in a folder of its own would be listed there with no page behind it. + assert.notEqual( + path.basename(path.dirname(sourceFilePath)), + name, + `${name} is @deprecated, so it cannot keep a folder of its own — move it into the folder of the export that replaces it` + ); + assert.equal( + await pathExists(documentPath), + false, + `${path.relative(root, documentPath)} must not exist: ${name} is @deprecated, and a deprecated export has no documentation page — delete it and its translations, and say what was renamed on the replacement's page` + ); + continue; + } + assert.equal( await fs.readFile(documentPath, 'utf8'), await renderEnglishDoc(name, sourceFilePath), @@ -57,3 +76,12 @@ for (const page of handWrittenPages) { } } } + +async function pathExists(target: string): Promise { + try { + await fs.access(target); + return true; + } catch { + return false; + } +} diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index 9e77b2e4..2c47c738 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -17,8 +17,11 @@ import { } from '../.vitepress/locales.mts'; import { packageSourceRoot } from '../.vitepress/shared.mts'; +import { PACKAGE_INDEX_FILE } from './commands/generateSkill/index.ts'; import { assertLlmsOutput } from './utils/assertLlmsOutput.ts'; import { assertSeoOutput } from './utils/assertSeoOutput.ts'; +import { collectDeprecatedExports } from './utils/collectDeprecatedExports.ts'; +import { collectPublicExports } from './utils/collectPublicExports.ts'; import { execWithOutput } from './utils/execWithOutput.ts'; import { getRootPath } from './utils/getRootPath.ts'; @@ -139,11 +142,12 @@ try { ) ).reduce((total, count) => total + count, 0); const guidePageCount = 11; + const renamedPageRedirectCount = 2; assert.equal( stubs.length, - (guidePageCount + referenceItemCount) * localeCount, - 'the legacy redirect set must cover every pre-flattening URL across all locales' + (guidePageCount + referenceItemCount + renamedPageRedirectCount) * localeCount, + 'the legacy redirect set must cover every pre-flattening and renamed URL across all locales' ); // Spot-check the shape itself: a renamed `from` would keep the count intact and @@ -155,10 +159,32 @@ try { 'mobile/roadmap.html', 'ko/core/utils/mergeRefs.html', 'ja/mobile/utils/isServer.html', + 'hooks/useCallbackOncePerRender.html', + 'ko/core/hooks/useCallbackOncePerRender.html', ]) { assert.equal(stubPaths.has(expected), true, `the legacy URL ${expected} must keep a redirect`); } + // A deprecated export lost its page, so forgetting its redirect would turn the published URL into a 404. + const publicExports = await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE)); + + for (const name of await collectDeprecatedExports(root, publicExports)) { + assert.equal( + ['hooks', 'components', 'utils'].some(category => stubPaths.has(`${category}/${name}.html`)), + true, + `${name} is @deprecated and has no page, so its old URL must redirect — add it to the renamed pages in legacyRedirects.mts` + ); + } + + // A renamed export has no page of its own, so its old URLs must land on the page of its new name. + for (const from of ['hooks/useCallbackOncePerRender.html', 'ja/core/hooks/useCallbackOncePerRender.html']) { + assert.equal( + stubs.find(stub => stub.from === from)?.to, + from.replace('core/', '').replace('useCallbackOncePerRender', 'useCallbackOnce'), + `${from} must redirect to the useCallbackOnce page` + ); + } + for (const { from, to } of stubs) { const stub = await fs.readFile(path.join(buildOutputDirectory, from), 'utf8'); assert.match(stub, /http-equiv="refresh"/, `${from} must redirect`); diff --git a/.scripts/verifySkill.ts b/.scripts/verifySkill.ts index 94c57411..9f40499a 100644 --- a/.scripts/verifySkill.ts +++ b/.scripts/verifySkill.ts @@ -4,7 +4,9 @@ import * as fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { readCatalogNames, readDeprecatedNames } from './commands/generateSkill/catalog.ts'; import { generateSkill, PACKAGE_INDEX_FILE, SKILL_DIRECTORY } from './commands/generateSkill/index.ts'; +import { collectDeprecatedExports } from './utils/collectDeprecatedExports.ts'; import { collectPublicExports } from './utils/collectPublicExports.ts'; import { getRootPath } from './utils/getRootPath.ts'; @@ -64,11 +66,21 @@ for (const [label, contents] of [ } const publicExports = await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE)); -const catalogNames = [...skill.matchAll(/^\| \[`([^`]+)`\]\(references\/\1\.md\) \| .+ \|$/gm)].map(match => match[1]); - -assert.deepEqual(catalogNames.toSorted(), publicExports, 'the catalog must list exactly the public exports'); - -for (const name of publicExports) { +const deprecatedExports = await collectDeprecatedExports(root, publicExports); +const documentedExports = publicExports.filter(name => !deprecatedExports.includes(name)); + +assert.deepEqual( + readCatalogNames(skill).toSorted(), + documentedExports, + 'the catalog must link exactly the public exports that are not @deprecated — run `yarn skill:gen`' +); +assert.deepEqual( + readDeprecatedNames(skill).toSorted(), + deprecatedExports, + 'the Deprecated section must list exactly the exports whose JSDoc is @deprecated — run `yarn skill:gen`' +); + +for (const name of documentedExports) { const page = await fs.readFile(path.join(skillDirectory, 'references', `${name}.md`), 'utf8'); // References are one level deep: a page that links to another local page would be read as a dead link. @@ -84,25 +96,16 @@ for (const name of publicExports) { ); } -// The skill has no "Deprecated" section by design; a deprecated export would be listed as if it were current. -const sourceFiles = await glob('packages/react-simplikit/src/**/*.{ts,tsx}', { - cwd: root, - ignore: ['**/*.spec.*', '**/*.test.*'], -}); - -for (const file of sourceFiles) { - assert.equal( - (await fs.readFile(path.join(root, file), 'utf8')).includes('@deprecated'), - false, - `${file} is @deprecated, but the skill catalog has no way to say so — un-deprecate it or add a Deprecated section to the template` - ); -} - // The hand-written table may only name catalog entries, or it drifts from the source. const commonNeeds = skill.slice(skill.indexOf('## Common needs'), skill.indexOf('## Workflow')); for (const [, token] of commonNeeds.matchAll(/`([^`]+)`/g)) { assert.equal(publicExports.includes(token), true, `"${token}" in the Common needs table is not a public export`); + assert.equal( + deprecatedExports.includes(token), + false, + `"${token}" in the Common needs table is @deprecated — name its replacement in template.md` + ); } async function readVerifiedSkill(directory: string, name: string): Promise { diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index 2cb4b4ac..122faac7 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -43,8 +43,15 @@ const RETIRED_MOBILE_PAGES = new Set([ 'subscribeKeyboardHeight', ]); +// A renamed export keeps no page under its old name, so its old URLs point at the new page: the +// current one and the pre-flattening one its folder used to produce. +const RENAMED_REFERENCE_REDIRECTS: RedirectPair[] = [ + { from: 'hooks/useCallbackOncePerRender.html', to: 'hooks/useCallbackOnce.html' }, + { from: 'core/hooks/useCallbackOncePerRender.html', to: 'hooks/useCallbackOnce.html' }, +]; + export function collectLegacyRedirects(): RedirectPair[] { - return [...GUIDE_REDIRECTS, ...collectReferenceRedirects()].flatMap(pair => [ + return [...GUIDE_REDIRECTS, ...collectReferenceRedirects(), ...RENAMED_REFERENCE_REDIRECTS].flatMap(pair => [ pair, ...localeDirectories.map(locale => ({ from: `${locale}/${pair.from}`, to: `${locale}/${pair.to}` })), ]); diff --git a/packages/plugin/skills/react-simplikit/SKILL.md b/packages/plugin/skills/react-simplikit/SKILL.md index 0004a1fd..a4d6afd5 100644 --- a/packages/plugin/skills/react-simplikit/SKILL.md +++ b/packages/plugin/skills/react-simplikit/SKILL.md @@ -76,7 +76,7 @@ Backticks in this table mark catalog entries only. | [`useAvoidKeyboard`](references/useAvoidKeyboard.md) | `useAvoidKeyboard` is a React hook that helps fixed-bottom elements avoid the on-screen keyboard. | | [`useBodyScrollLock`](references/useBodyScrollLock.md) | `useBodyScrollLock` is a React hook that locks body scroll while the component is mounted. | | [`useBooleanState`](references/useBooleanState.md) | `useBooleanState` is a React hook that simplifies managing a boolean state. | -| [`useCallbackOncePerRender`](references/useCallbackOncePerRender.md) | `useCallbackOncePerRender` is a React hook that ensures a callback function is executed only once, regardless of how many times it's called. | +| [`useCallbackOnce`](references/useCallbackOnce.md) | `useCallbackOnce` is a React hook that runs a callback only once until `deps` change, no matter how many times the returned function is called. | | [`useConditionalEffect`](references/useConditionalEffect.md) | `useConditionalEffect` is a React hook that conditionally executes effects based on a predicate function. | | [`useControlledState`](references/useControlledState.md) | `useControlledState` is a React hook that allows you to control both controlled and uncontrolled states. | | [`useCounter`](references/useCounter.md) | `useCounter` is a React hook that manages a numeric counter state with increment, decrement, and reset capabilities. | @@ -140,6 +140,14 @@ Backticks in this table mark catalog entries only. | [`mergeRefs`](references/mergeRefs.md) | This function takes multiple refs (RefObject or RefCallback) and returns a single ref that updates all provided refs. | | [`subscribeKeyboardHeight`](references/subscribeKeyboardHeight.md) | `subscribeKeyboardHeight` is a utility function that subscribes to changes in the on-screen keyboard height. | +### Deprecated + +These still work but are kept only for backward compatibility. Do not use them in new code; each row names the replacement. + +| Name | Notice | +| --- | --- | +| `useCallbackOncePerRender` | Use `useCallbackOnce` instead. | + ## Learn more - Docs: https://react-simplikit.slash.page diff --git a/packages/plugin/skills/react-simplikit/references/useCallbackOnce.md b/packages/plugin/skills/react-simplikit/references/useCallbackOnce.md new file mode 100644 index 00000000..b4c5b0d9 --- /dev/null +++ b/packages/plugin/skills/react-simplikit/references/useCallbackOnce.md @@ -0,0 +1,67 @@ +# useCallbackOnce + +`useCallbackOnce` is a React hook that runs a callback only once until `deps` change, no matter how many times the returned function is called. +This is useful for one-time operations that should not be repeated, even if the component re-renders. +In v0 this hook was named `useCallbackOncePerRender`. The old name still works but is deprecated. + +## Interface + +```ts +function useCallbackOnce void>( + callback: F, + deps: DependencyList +): (...args: Parameters) => void; +``` + +### Parameters + + + + + +### Return Value + + + +## Example + +```tsx +import { useCallbackOnce } from 'react-simplikit'; + +function Component() { + const handleOneTimeEvent = useCallbackOnce(() => { + console.log('This will only run once'); + }, []); + + return ; +} +``` + +```tsx +// With dependencies +function TrackingComponent({ userId }: { userId: string }) { + const trackUserVisit = useCallbackOnce(() => { + analytics.trackVisit(userId); + }, [userId]); + + useEffect(() => { + trackUserVisit(); + }, [trackUserVisit, userId]); + + return
User page
; +} +``` diff --git a/packages/plugin/skills/react-simplikit/references/useCallbackOncePerRender.md b/packages/plugin/skills/react-simplikit/references/useCallbackOncePerRender.md deleted file mode 100644 index 00af226b..00000000 --- a/packages/plugin/skills/react-simplikit/references/useCallbackOncePerRender.md +++ /dev/null @@ -1,66 +0,0 @@ -# useCallbackOncePerRender - -`useCallbackOncePerRender` is a React hook that ensures a callback function is executed only once, regardless of how many times it's called. -This is useful for one-time operations that should not be repeated, even if the component re-renders. - -## Interface - -```ts -function useCallbackOncePerRender void>( - callback: () => void, - deps: DependencyList -): (...args: any[]) => void; -``` - -### Parameters - - - - - -### Return Value - - - -## Example - -```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; - -function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { - console.log('This will only run once'); - }, []); - - return ; -} -``` - -```tsx -// With dependencies -function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { - analytics.trackVisit(userId); - }, [userId]); - - useEffect(() => { - trackUserVisit(); - }, [trackUserVisit]); - - return
User page
; -} -``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOnce/es/useCallbackOnce.md b/packages/react-simplikit/src/hooks/useCallbackOnce/es/useCallbackOnce.md new file mode 100644 index 00000000..100d9d2d --- /dev/null +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/es/useCallbackOnce.md @@ -0,0 +1,67 @@ +# useCallbackOnce + +`useCallbackOnce` es un Hook de React que ejecuta un callback una sola vez hasta que cambie `deps`, sin importar cuántas veces llames a la función que devuelve. +Te resulta útil para operaciones que solo deben ejecutarse una vez, incluso si el componente vuelve a renderizarse. +En v0 este Hook se llamaba `useCallbackOncePerRender`. El nombre anterior sigue funcionando, pero está obsoleto. + +## Interfaz + +```ts +function useCallbackOnce void>( + callback: F, + deps: DependencyList +): (...args: Parameters) => void; +``` + +### Parámetros + + + + + +### Valor de retorno + + + +## Ejemplo + +```tsx +import { useCallbackOnce } from 'react-simplikit'; + +function Component() { + const handleOneTimeEvent = useCallbackOnce(() => { + console.log('Esto solo se ejecutará una vez'); + }, []); + + return ; +} +``` + +```tsx +// Con dependencias +function TrackingComponent({ userId }: { userId: string }) { + const trackUserVisit = useCallbackOnce(() => { + analytics.trackVisit(userId); + }, [userId]); + + useEffect(() => { + trackUserVisit(); + }, [trackUserVisit, userId]); + + return
Página del usuario
; +} +``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/index.ts b/packages/react-simplikit/src/hooks/useCallbackOnce/index.ts similarity index 56% rename from packages/react-simplikit/src/hooks/useCallbackOncePerRender/index.ts rename to packages/react-simplikit/src/hooks/useCallbackOnce/index.ts index c4f884e5..892bf361 100644 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/index.ts +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/index.ts @@ -1 +1,2 @@ +export { useCallbackOnce } from './useCallbackOnce.ts'; export { useCallbackOncePerRender } from './useCallbackOncePerRender.ts'; diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/ja/useCallbackOncePerRender.md b/packages/react-simplikit/src/hooks/useCallbackOnce/ja/useCallbackOnce.md similarity index 50% rename from packages/react-simplikit/src/hooks/useCallbackOncePerRender/ja/useCallbackOncePerRender.md rename to packages/react-simplikit/src/hooks/useCallbackOnce/ja/useCallbackOnce.md index ef860a4c..f2dc619c 100644 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/ja/useCallbackOncePerRender.md +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/ja/useCallbackOnce.md @@ -1,15 +1,16 @@ -# useCallbackOncePerRender +# useCallbackOnce -`useCallbackOncePerRender` は、何度呼び出されてもコールバック関数を一度だけ実行する React フックです。 +`useCallbackOnce` は、返された関数が何度呼び出されても、`deps` が変わるまではコールバック関数を一度だけ実行する React フックです。 コンポーネントが再レンダリングされても繰り返すべきでない、一度限りの処理に便利です。 +v0 では、このフックは `useCallbackOncePerRender` という名前でした。以前の名前も引き続き動作しますが、非推奨です。 ## インターフェース ```ts -function useCallbackOncePerRender void>( - callback: () => void, +function useCallbackOnce void>( + callback: F, deps: DependencyList -): (...args: any[]) => void; +): (...args: Parameters) => void; ``` ### パラメータ @@ -17,8 +18,8 @@ function useCallbackOncePerRender void>( void>( ## 使用例 ```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; +import { useCallbackOnce } from 'react-simplikit'; function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { + const handleOneTimeEvent = useCallbackOnce(() => { console.log('This will only run once'); }, []); @@ -53,13 +54,13 @@ function Component() { ```tsx // 依存関係を指定する場合 function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { + const trackUserVisit = useCallbackOnce(() => { analytics.trackVisit(userId); }, [userId]); useEffect(() => { trackUserVisit(); - }, [trackUserVisit]); + }, [trackUserVisit, userId]); return
User page
; } diff --git a/packages/react-simplikit/src/hooks/useCallbackOnce/ko/useCallbackOnce.md b/packages/react-simplikit/src/hooks/useCallbackOnce/ko/useCallbackOnce.md new file mode 100644 index 00000000..1ba0bdc2 --- /dev/null +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/ko/useCallbackOnce.md @@ -0,0 +1,65 @@ +# useCallbackOnce + +`deps`가 바뀌기 전까지는 반환된 함수를 몇 번 호출하든 콜백을 한 번만 실행하는 리액트 훅이에요. 이는 컴포넌트가 다시 렌더링되더라도 반복되지 말아야 하는 일회성 작업에 유용해요. v0에서는 이 훅의 이름이 `useCallbackOncePerRender`였어요. 이전 이름도 계속 동작하지만 deprecated 상태예요. + +## 인터페이스 + +```ts +function useCallbackOnce void>( + callback: F, + deps: DependencyList +): (...args: Parameters) => void; +``` + +### 파라미터 + + + + + +### 반환 값 + + + +## 예시 + +```tsx +import { useCallbackOnce } from 'react-simplikit'; + +function Component() { + const handleOneTimeEvent = useCallbackOnce(() => { + console.log('이것은 한 번만 실행될 거예요'); + }, []); + + return ; +} +``` + +```tsx +// 의존성과 함께 사용하는 경우 +function TrackingComponent({ userId }: { userId: string }) { + const trackUserVisit = useCallbackOnce(() => { + analytics.trackVisit(userId); + }, [userId]); + + useEffect(() => { + trackUserVisit(); + }, [trackUserVisit, userId]); + + return
사용자 페이지
; +} +``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.md b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.md new file mode 100644 index 00000000..b4c5b0d9 --- /dev/null +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.md @@ -0,0 +1,67 @@ +# useCallbackOnce + +`useCallbackOnce` is a React hook that runs a callback only once until `deps` change, no matter how many times the returned function is called. +This is useful for one-time operations that should not be repeated, even if the component re-renders. +In v0 this hook was named `useCallbackOncePerRender`. The old name still works but is deprecated. + +## Interface + +```ts +function useCallbackOnce void>( + callback: F, + deps: DependencyList +): (...args: Parameters) => void; +``` + +### Parameters + + + + + +### Return Value + + + +## Example + +```tsx +import { useCallbackOnce } from 'react-simplikit'; + +function Component() { + const handleOneTimeEvent = useCallbackOnce(() => { + console.log('This will only run once'); + }, []); + + return ; +} +``` + +```tsx +// With dependencies +function TrackingComponent({ userId }: { userId: string }) { + const trackUserVisit = useCallbackOnce(() => { + analytics.trackVisit(userId); + }, [userId]); + + useEffect(() => { + trackUserVisit(); + }, [trackUserVisit, userId]); + + return
User page
; +} +``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.spec.ts b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.test.ts similarity index 73% rename from packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.spec.ts rename to packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.test.ts index 102feb6c..e3cf3507 100644 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.spec.ts +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import { renderHookSSR } from '../../_internal/test-utils/renderHookSSR.tsx'; +import { useCallbackOnce } from './useCallbackOnce.ts'; import { useCallbackOncePerRender } from './useCallbackOncePerRender.ts'; function useCaller(callback: (...args: any) => any, deps: DependencyList) { @@ -13,20 +14,17 @@ function useCaller(callback: (...args: any) => any, deps: DependencyList) { }, deps); } -describe('useCallbackOncePerRender', () => { +describe('useCallbackOnce', () => { it('is safe on server side rendering', () => { const mockFn = vi.fn(); - renderHookSSR.serverOnly(() => useCallbackOncePerRender(mockFn, [])); + renderHookSSR.serverOnly(() => useCallbackOnce(mockFn, [])); }); it('should execute callback only once', async () => { const mockFn = vi.fn(); - const { rerender } = await renderHookSSR( - ({ effect }) => useCaller(useCallbackOncePerRender(mockFn, []), [effect]), - { - initialProps: { effect: 0 }, - } - ); + const { rerender } = await renderHookSSR(({ effect }) => useCaller(useCallbackOnce(mockFn, []), [effect]), { + initialProps: { effect: 0 }, + }); rerender({ effect: 1 }); rerender({ effect: 2 }); @@ -38,7 +36,7 @@ describe('useCallbackOncePerRender', () => { it('should reset and execute again when dependencies change', async () => { const mockFn = vi.fn(); const { rerender } = await renderHookSSR( - ({ effect, call }) => useCaller(useCallbackOncePerRender(mockFn, [call]), [effect, call]), + ({ effect, call }) => useCaller(useCallbackOnce(mockFn, [call]), [effect, call]), { initialProps: { effect: 0, call: 0 }, } @@ -55,8 +53,12 @@ describe('useCallbackOncePerRender', () => { it('should pass arguments to callback', async () => { const mockFn = vi.fn(); - const { result } = await renderHookSSR(() => useCallbackOncePerRender(mockFn, [])); + const { result } = await renderHookSSR(() => useCallbackOnce(mockFn, [])); result.current('test', 123); expect(mockFn).toHaveBeenCalledWith('test', 123); }); + + it('is also exported under the deprecated name useCallbackOncePerRender', () => { + expect(useCallbackOncePerRender).toBe(useCallbackOnce); + }); }); diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.ts similarity index 60% rename from packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts rename to packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.ts index ded97fc9..2fc02519 100644 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOnce.ts @@ -5,20 +5,21 @@ import { usePreservedCallback } from '../usePreservedCallback/index.ts'; /** * @description - * `useCallbackOncePerRender` is a React hook that ensures a callback function is executed only once, regardless of how many times it's called. + * `useCallbackOnce` is a React hook that runs a callback only once until `deps` change, no matter how many times the returned function is called. * This is useful for one-time operations that should not be repeated, even if the component re-renders. + * In v0 this hook was named `useCallbackOncePerRender`. The old name still works but is deprecated. * * @template {(...args: any[]) => void} F - The type of the callback function. - * @param {() => void} callback - The callback function to be executed once. - * @param {DependencyList} deps - Dependencies array that will trigger a new one-time execution when changed. + * @param {F} callback - The callback function to be executed once. It receives the arguments passed to the returned function. + * @param {DependencyList} deps - Dependencies array. When it changes, the returned function can run the callback once more. * - * @returns {(...args: any[]) => void} A memoized function that will only execute once until dependencies change. + * @returns {(...args: Parameters) => void} A function whose reference never changes. It runs the callback only once until `deps` change. * * @example - * import { useCallbackOncePerRender } from 'react-simplikit'; + * import { useCallbackOnce } from 'react-simplikit'; * * function Component() { - * const handleOneTimeEvent = useCallbackOncePerRender(() => { + * const handleOneTimeEvent = useCallbackOnce(() => { * console.log('This will only run once'); * }, []); * @@ -28,18 +29,18 @@ import { usePreservedCallback } from '../usePreservedCallback/index.ts'; * @example * // With dependencies * function TrackingComponent({ userId }: { userId: string }) { - * const trackUserVisit = useCallbackOncePerRender(() => { + * const trackUserVisit = useCallbackOnce(() => { * analytics.trackVisit(userId); * }, [userId]); * * useEffect(() => { * trackUserVisit(); - * }, [trackUserVisit]); + * }, [trackUserVisit, userId]); * * return
User page
; * } */ -export function useCallbackOncePerRender void>(callback: F, deps: DependencyList) { +export function useCallbackOnce void>(callback: F, deps: DependencyList) { // Same reason as `useAsyncEffect`: the body itself is compiler-clean, but React Compiler // bails on any function carrying a React ESLint suppression, and the // `react-hooks/exhaustive-deps` suppression below is unavoidable for a caller-supplied `deps`. diff --git a/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOncePerRender.ts b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOncePerRender.ts new file mode 100644 index 00000000..ae6593b1 --- /dev/null +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/useCallbackOncePerRender.ts @@ -0,0 +1,9 @@ +import { useCallbackOnce } from './useCallbackOnce.ts'; + +/** + * @description + * `useCallbackOncePerRender` is the name `useCallbackOnce` had in v0. + * + * @deprecated Use `useCallbackOnce` instead. + */ +export const useCallbackOncePerRender = useCallbackOnce; diff --git a/packages/react-simplikit/src/hooks/useCallbackOnce/zh-Hans/useCallbackOnce.md b/packages/react-simplikit/src/hooks/useCallbackOnce/zh-Hans/useCallbackOnce.md new file mode 100644 index 00000000..1cd584dd --- /dev/null +++ b/packages/react-simplikit/src/hooks/useCallbackOnce/zh-Hans/useCallbackOnce.md @@ -0,0 +1,65 @@ +# useCallbackOnce + +`useCallbackOnce` 是一个 React Hook,在 `deps` 变化之前,无论返回的函数被调用多少次,它都只会执行一次回调函数。它适用于那些即使组件重复渲染也不应重复执行的一次性操作。在 v0 中,这个 Hook 名为 `useCallbackOncePerRender`。旧名称仍然可用,但已弃用。 + +## 接口 + +```ts +function useCallbackOnce void>( + callback: F, + deps: DependencyList +): (...args: Parameters) => void; +``` + +### 参数 + + + + + +### 返回值 + + + +## 示例 + +```tsx +import { useCallbackOnce } from 'react-simplikit'; + +function Component() { + const handleOneTimeEvent = useCallbackOnce(() => { + console.log('This will only run once'); + }, []); + + return ; +} +``` + +```tsx +// With dependencies +function TrackingComponent({ userId }: { userId: string }) { + const trackUserVisit = useCallbackOnce(() => { + analytics.trackVisit(userId); + }, [userId]); + + useEffect(() => { + trackUserVisit(); + }, [trackUserVisit, userId]); + + return
User page
; +} +``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/es/useCallbackOncePerRender.md b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/es/useCallbackOncePerRender.md deleted file mode 100644 index 21699e92..00000000 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/es/useCallbackOncePerRender.md +++ /dev/null @@ -1,66 +0,0 @@ -# useCallbackOncePerRender - -`useCallbackOncePerRender` es un Hook de React que garantiza que un callback se ejecute una sola vez, sin importar cuántas veces lo llames. -Te resulta útil para operaciones que solo deben ejecutarse una vez, incluso si el componente vuelve a renderizarse. - -## Interfaz - -```ts -function useCallbackOncePerRender void>( - callback: () => void, - deps: DependencyList -): (...args: any[]) => void; -``` - -### Parámetros - - - - - -### Valor de retorno - - - -## Ejemplo - -```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; - -function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { - console.log('Esto solo se ejecutará una vez'); - }, []); - - return ; -} -``` - -```tsx -// Con dependencias -function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { - analytics.trackVisit(userId); - }, [userId]); - - useEffect(() => { - trackUserVisit(); - }, [trackUserVisit]); - - return
Página del usuario
; -} -``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/ko/useCallbackOncePerRender.md b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/ko/useCallbackOncePerRender.md deleted file mode 100644 index 57eaec16..00000000 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/ko/useCallbackOncePerRender.md +++ /dev/null @@ -1,65 +0,0 @@ -# useCallbackOncePerRender - -리액트 훅으로, 콜백 함수가 한 번만 실행되도록 보장해요. 몇 번 호출되든 간에 단 한 번만 실행돼요. 이는 컴포넌트가 다시 렌더링되더라도 반복되지 말아야 하는 일회성 작업에 유용해요. - -## 인터페이스 - -```ts -function useCallbackOncePerRender void>( - callback: () => void, - deps: DependencyList -): (...args: any[]) => void; -``` - -### 파라미터 - - - - - -### 반환 값 - - - -## 예시 - -```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; - -function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { - console.log('이것은 한 번만 실행될 거예요'); - }, []); - - return ; -} -``` - -```tsx -// 의존성과 함께 사용하는 경우 -function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { - analytics.trackVisit(userId); - }, [userId]); - - useEffect(() => { - trackUserVisit(); - }, [trackUserVisit]); - - return
사용자 페이지
; -} -``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.md b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.md deleted file mode 100644 index 00af226b..00000000 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.md +++ /dev/null @@ -1,66 +0,0 @@ -# useCallbackOncePerRender - -`useCallbackOncePerRender` is a React hook that ensures a callback function is executed only once, regardless of how many times it's called. -This is useful for one-time operations that should not be repeated, even if the component re-renders. - -## Interface - -```ts -function useCallbackOncePerRender void>( - callback: () => void, - deps: DependencyList -): (...args: any[]) => void; -``` - -### Parameters - - - - - -### Return Value - - - -## Example - -```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; - -function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { - console.log('This will only run once'); - }, []); - - return ; -} -``` - -```tsx -// With dependencies -function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { - analytics.trackVisit(userId); - }, [userId]); - - useEffect(() => { - trackUserVisit(); - }, [trackUserVisit]); - - return
User page
; -} -``` diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/zh-Hans/useCallbackOncePerRender.md b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/zh-Hans/useCallbackOncePerRender.md deleted file mode 100644 index 9324d643..00000000 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/zh-Hans/useCallbackOncePerRender.md +++ /dev/null @@ -1,65 +0,0 @@ -# useCallbackOncePerRender - -`useCallbackOncePerRender` 是一个确保回调函数无论被调用多少次都只执行一次的 React Hook。它适用于那些即使组件重复渲染也不应重复执行的一次性操作。 - -## 接口 - -```ts -function useCallbackOncePerRender void>( - callback: () => void, - deps: DependencyList -): (...args: any[]) => void; -``` - -### 参数 - - - - - -### 返回值 - - - -## 示例 - -```tsx -import { useCallbackOncePerRender } from 'react-simplikit'; - -function Component() { - const handleOneTimeEvent = useCallbackOncePerRender(() => { - console.log('This will only run once'); - }, []); - - return ; -} -``` - -```tsx -// With dependencies -function TrackingComponent({ userId }: { userId: string }) { - const trackUserVisit = useCallbackOncePerRender(() => { - analytics.trackVisit(userId); - }, [userId]); - - useEffect(() => { - trackUserVisit(); - }, [trackUserVisit]); - - return
User page
; -} -``` diff --git a/packages/react-simplikit/src/index.ts b/packages/react-simplikit/src/index.ts index b0fc24fd..99337942 100644 --- a/packages/react-simplikit/src/index.ts +++ b/packages/react-simplikit/src/index.ts @@ -5,7 +5,7 @@ export { useAsyncEffect } from './hooks/useAsyncEffect/index.ts'; export { useAvoidKeyboard } from './hooks/useAvoidKeyboard/index.ts'; export { useBodyScrollLock } from './hooks/useBodyScrollLock/index.ts'; export { useBooleanState } from './hooks/useBooleanState/index.ts'; -export { useCallbackOncePerRender } from './hooks/useCallbackOncePerRender/index.ts'; +export { useCallbackOnce, useCallbackOncePerRender } from './hooks/useCallbackOnce/index.ts'; export { useConditionalEffect } from './hooks/useConditionalEffect/index.ts'; export { useControlledState } from './hooks/useControlledState/index.ts'; export { useCounter } from './hooks/useCounter/index.ts'; From 83ae1a3aef35d0ce47872e8312fe9923b6fadcd0 Mon Sep 17 00:00:00 2001 From: hyesung oh Date: Wed, 7 Oct 2026 18:14:21 +0900 Subject: [PATCH 05/10] feat(usePageVisibilityEffect): add usePageVisibilityEffect and deprecate useVisibilityEvent (#546) --- .changeset/page-visibility-effect.md | 5 +++++ .scripts/commands/generateSkill/template.md | 2 +- .scripts/verifyDocsI18n.ts | 21 ++++++++++++------- .vitepress/libs/legacyRedirects.mts | 2 ++ .../plugin/skills/react-simplikit/SKILL.md | 5 +++-- ...ityEvent.md => usePageVisibilityEffect.md} | 11 +++++----- .../useImpressionRef/useImpressionRef.ts | 4 ++-- .../usePageVisibility/usePageVisibility.ts | 2 +- .../es/usePageVisibilityEffect.md} | 11 +++++----- .../hooks/usePageVisibilityEffect/index.ts | 2 ++ .../ja/usePageVisibilityEffect.md} | 11 +++++----- .../ko/usePageVisibilityEffect.md} | 10 ++++----- .../usePageVisibilityEffect.md} | 11 +++++----- .../usePageVisibilityEffect.test.ts} | 17 +++++++++------ .../usePageVisibilityEffect.ts} | 12 +++++++---- .../useVisibilityEvent.ts | 9 ++++++++ .../zh-Hans/usePageVisibilityEffect.md} | 10 ++++----- .../src/hooks/useVisibilityEvent/index.ts | 1 - packages/react-simplikit/src/index.ts | 2 +- 19 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 .changeset/page-visibility-effect.md rename packages/plugin/skills/react-simplikit/references/{useVisibilityEvent.md => usePageVisibilityEffect.md} (71%) rename packages/react-simplikit/src/hooks/{useVisibilityEvent/es/useVisibilityEvent.md => usePageVisibilityEffect/es/usePageVisibilityEffect.md} (71%) create mode 100644 packages/react-simplikit/src/hooks/usePageVisibilityEffect/index.ts rename packages/react-simplikit/src/hooks/{useVisibilityEvent/ja/useVisibilityEvent.md => usePageVisibilityEffect/ja/usePageVisibilityEffect.md} (69%) rename packages/react-simplikit/src/hooks/{useVisibilityEvent/ko/useVisibilityEvent.md => usePageVisibilityEffect/ko/usePageVisibilityEffect.md} (76%) rename packages/react-simplikit/src/hooks/{useVisibilityEvent/useVisibilityEvent.md => usePageVisibilityEffect/usePageVisibilityEffect.md} (71%) rename packages/react-simplikit/src/hooks/{useVisibilityEvent/useVisibilityEvent.spec.ts => usePageVisibilityEffect/usePageVisibilityEffect.test.ts} (70%) rename packages/react-simplikit/src/hooks/{useVisibilityEvent/useVisibilityEvent.ts => usePageVisibilityEffect/usePageVisibilityEffect.ts} (73%) create mode 100644 packages/react-simplikit/src/hooks/usePageVisibilityEffect/useVisibilityEvent.ts rename packages/react-simplikit/src/hooks/{useVisibilityEvent/zh-Hans/useVisibilityEvent.md => usePageVisibilityEffect/zh-Hans/usePageVisibilityEffect.md} (71%) delete mode 100644 packages/react-simplikit/src/hooks/useVisibilityEvent/index.ts diff --git a/.changeset/page-visibility-effect.md b/.changeset/page-visibility-effect.md new file mode 100644 index 00000000..c8094190 --- /dev/null +++ b/.changeset/page-visibility-effect.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': minor +--- + +Add `usePageVisibilityEffect`, which runs a callback when the page visibility changes and pairs by name with `usePageVisibility`, which returns the same value as state. `useVisibilityEvent` is deprecated in favor of `usePageVisibilityEffect` and still works as before, since it is now the same function under its old name. This reverses the note from #435 that `useVisibilityEvent` is no longer deprecated. It is deprecated again because of the rename, not because it is going away: it keeps working throughout v1 and may be removed in a later major version. diff --git a/.scripts/commands/generateSkill/template.md b/.scripts/commands/generateSkill/template.md index b4b1ad5e..1df730f9 100644 --- a/.scripts/commands/generateSkill/template.md +++ b/.scripts/commands/generateSkill/template.md @@ -43,7 +43,7 @@ Backticks in this table mark catalog entries only. | Long press / double click | `useLongPress` / `useDoubleClick` | | Element enters or leaves the viewport | `useIntersectionObserver` | | How long an element stayed visible (impression tracking) | `useImpressionRef` / `ImpressionArea` | -| React to tab / page visibility changes with a callback | `useVisibilityEvent` | +| React to tab / page visibility changes with a callback | `usePageVisibilityEffect` | | Read the current tab / page visibility as state | `usePageVisibility` | | Device location | `useGeolocation` | | Combine multiple refs into one | `mergeRefs` | diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index 2c47c738..7d5aeae1 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -142,7 +142,7 @@ try { ) ).reduce((total, count) => total + count, 0); const guidePageCount = 11; - const renamedPageRedirectCount = 2; + const renamedPageRedirectCount = 4; assert.equal( stubs.length, @@ -161,6 +161,8 @@ try { 'ja/mobile/utils/isServer.html', 'hooks/useCallbackOncePerRender.html', 'ko/core/hooks/useCallbackOncePerRender.html', + 'hooks/useVisibilityEvent.html', + 'ko/core/hooks/useVisibilityEvent.html', ]) { assert.equal(stubPaths.has(expected), true, `the legacy URL ${expected} must keep a redirect`); } @@ -177,12 +179,17 @@ try { } // A renamed export has no page of its own, so its old URLs must land on the page of its new name. - for (const from of ['hooks/useCallbackOncePerRender.html', 'ja/core/hooks/useCallbackOncePerRender.html']) { - assert.equal( - stubs.find(stub => stub.from === from)?.to, - from.replace('core/', '').replace('useCallbackOncePerRender', 'useCallbackOnce'), - `${from} must redirect to the useCallbackOnce page` - ); + for (const [oldName, newName] of [ + ['useCallbackOncePerRender', 'useCallbackOnce'], + ['useVisibilityEvent', 'usePageVisibilityEffect'], + ]) { + for (const from of [`hooks/${oldName}.html`, `ja/core/hooks/${oldName}.html`]) { + assert.equal( + stubs.find(stub => stub.from === from)?.to, + from.replace('core/', '').replace(oldName, newName), + `${from} must redirect to the ${newName} page` + ); + } } for (const { from, to } of stubs) { diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index 122faac7..436e042d 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -48,6 +48,8 @@ const RETIRED_MOBILE_PAGES = new Set([ const RENAMED_REFERENCE_REDIRECTS: RedirectPair[] = [ { from: 'hooks/useCallbackOncePerRender.html', to: 'hooks/useCallbackOnce.html' }, { from: 'core/hooks/useCallbackOncePerRender.html', to: 'hooks/useCallbackOnce.html' }, + { from: 'hooks/useVisibilityEvent.html', to: 'hooks/usePageVisibilityEffect.html' }, + { from: 'core/hooks/useVisibilityEvent.html', to: 'hooks/usePageVisibilityEffect.html' }, ]; export function collectLegacyRedirects(): RedirectPair[] { diff --git a/packages/plugin/skills/react-simplikit/SKILL.md b/packages/plugin/skills/react-simplikit/SKILL.md index a4d6afd5..28292be3 100644 --- a/packages/plugin/skills/react-simplikit/SKILL.md +++ b/packages/plugin/skills/react-simplikit/SKILL.md @@ -43,7 +43,7 @@ Backticks in this table mark catalog entries only. | Long press / double click | `useLongPress` / `useDoubleClick` | | Element enters or leaves the viewport | `useIntersectionObserver` | | How long an element stayed visible (impression tracking) | `useImpressionRef` / `ImpressionArea` | -| React to tab / page visibility changes with a callback | `useVisibilityEvent` | +| React to tab / page visibility changes with a callback | `usePageVisibilityEffect` | | Read the current tab / page visibility as state | `usePageVisibility` | | Device location | `useGeolocation` | | Combine multiple refs into one | `mergeRefs` | @@ -99,6 +99,7 @@ Backticks in this table mark catalog entries only. | [`useNetworkStatus`](references/useNetworkStatus.md) | `useNetworkStatus` is a React hook that provides access to the Network Information API. | | [`useOutsideClickEffect`](references/useOutsideClickEffect.md) | `useOutsideClickEffect` is a React hook that triggers a callback when a click event occurs outside the specified container(s). | | [`usePageVisibility`](references/usePageVisibility.md) | `usePageVisibility` is a React hook that detects page visibility changes. | +| [`usePageVisibilityEffect`](references/usePageVisibilityEffect.md) | `usePageVisibilityEffect` is a React hook that listens to changes in the document's visibility state and triggers a callback. | | [`usePreservedCallback`](references/usePreservedCallback.md) | `usePreservedCallback` is a React hook that maintains a stable reference to a callback function while ensuring it always has access to the latest state or props. | | [`usePreservedReference`](references/usePreservedReference.md) | `usePreservedReference` is a React hook that helps maintain the reference of a value when it hasn't changed, while ensuring you can safely use the latest state. | | [`usePrevious`](references/usePrevious.md) | `usePrevious` is a React hook that returns the previous value of the input state. | @@ -112,7 +113,6 @@ Backticks in this table mark catalog entries only. | [`useThrottledValue`](references/useThrottledValue.md) | `useThrottledValue` is a React hook that returns a throttled copy of the given value. | | [`useTimeout`](references/useTimeout.md) | `useTimeout` is a React hook that executes a callback function after a specified delay. | | [`useToggle`](references/useToggle.md) | `useToggle` is a React hook that simplifies managing a boolean state. | -| [`useVisibilityEvent`](references/useVisibilityEvent.md) | `useVisibilityEvent` is a React hook that listens to changes in the document's visibility state and triggers a callback. | | [`useVisualViewport`](references/useVisualViewport.md) | `useVisualViewport` is a React hook that tracks Visual Viewport changes. | ### components @@ -147,6 +147,7 @@ These still work but are kept only for backward compatibility. Do not use them i | Name | Notice | | --- | --- | | `useCallbackOncePerRender` | Use `useCallbackOnce` instead. | +| `useVisibilityEvent` | Use `usePageVisibilityEffect` instead. | ## Learn more diff --git a/packages/plugin/skills/react-simplikit/references/useVisibilityEvent.md b/packages/plugin/skills/react-simplikit/references/usePageVisibilityEffect.md similarity index 71% rename from packages/plugin/skills/react-simplikit/references/useVisibilityEvent.md rename to packages/plugin/skills/react-simplikit/references/usePageVisibilityEffect.md index 52115cc8..24882458 100644 --- a/packages/plugin/skills/react-simplikit/references/useVisibilityEvent.md +++ b/packages/plugin/skills/react-simplikit/references/usePageVisibilityEffect.md @@ -1,11 +1,12 @@ -# useVisibilityEvent +# usePageVisibilityEffect -`useVisibilityEvent` is a React hook that listens to changes in the document's visibility state and triggers a callback. +`usePageVisibilityEffect` is a React hook that listens to changes in the document's visibility state and triggers a callback. +In v0 this hook was named `useVisibilityEvent`. The old name still works but is deprecated. ## Interface ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +44,10 @@ This function does not return anything. ## Example ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`Document is now ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts index c2f634a6..b6f88a74 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts +++ b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts @@ -2,8 +2,8 @@ import { useEffect, useRef } from 'react'; import { useDebouncedCallback } from '../useDebouncedCallback/useDebouncedCallback.ts'; import { useIntersectionObserver } from '../useIntersectionObserver/index.ts'; +import { usePageVisibilityEffect } from '../usePageVisibilityEffect/index.ts'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; -import { useVisibilityEvent } from '../useVisibilityEvent/index.ts'; export type UseImpressionRefOptions = Partial<{ onImpressionStart: () => void; @@ -96,7 +96,7 @@ export function useImpressionRef({ [impressionEndHandler] ); - useVisibilityEvent(documentVisible => { + usePageVisibilityEffect(documentVisible => { if (!isIntersectingRef.current) { return; } diff --git a/packages/react-simplikit/src/hooks/usePageVisibility/usePageVisibility.ts b/packages/react-simplikit/src/hooks/usePageVisibility/usePageVisibility.ts index bb90e3d0..8e659e9b 100644 --- a/packages/react-simplikit/src/hooks/usePageVisibility/usePageVisibility.ts +++ b/packages/react-simplikit/src/hooks/usePageVisibility/usePageVisibility.ts @@ -24,7 +24,7 @@ export type PageVisibility = { * It monitors when the user switches tabs or minimizes the browser using the Page Visibility API. * Useful for pausing/resuming animations, videos, or background tasks to improve performance and the user experience. * - * @see `useVisibilityEvent` runs a callback on each change instead of returning state. + * @see `usePageVisibilityEffect` runs a callback on each change instead of returning state. * * @returns {PageVisibility} Page visibility information * - isVisible `boolean` - `true` if the page is currently visible to the user diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/es/useVisibilityEvent.md b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/es/usePageVisibilityEffect.md similarity index 71% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/es/useVisibilityEvent.md rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/es/usePageVisibilityEffect.md index 9dfb4e80..2f0db457 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/es/useVisibilityEvent.md +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/es/usePageVisibilityEffect.md @@ -1,11 +1,12 @@ -# useVisibilityEvent +# usePageVisibilityEffect -`useVisibilityEvent` es un Hook de React que escucha los cambios en el estado de visibilidad del documento y ejecuta un callback. +`usePageVisibilityEffect` es un Hook de React que escucha los cambios en el estado de visibilidad del documento y ejecuta un callback. +En v0 este Hook se llamaba `useVisibilityEvent`. El nombre anterior sigue funcionando, pero está obsoleto. ## Interfaz ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +44,10 @@ Esta función no devuelve ningún valor. ## Ejemplo ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`El estado del documento ahora es ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/usePageVisibilityEffect/index.ts b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/index.ts new file mode 100644 index 00000000..f1c21631 --- /dev/null +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/index.ts @@ -0,0 +1,2 @@ +export { usePageVisibilityEffect } from './usePageVisibilityEffect.ts'; +export { useVisibilityEvent } from './useVisibilityEvent.ts'; diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/ja/useVisibilityEvent.md b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/ja/usePageVisibilityEffect.md similarity index 69% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/ja/useVisibilityEvent.md rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/ja/usePageVisibilityEffect.md index 4cf5116c..8ce1a705 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/ja/useVisibilityEvent.md +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/ja/usePageVisibilityEffect.md @@ -1,11 +1,12 @@ -# useVisibilityEvent +# usePageVisibilityEffect -`useVisibilityEvent` は、ドキュメントの表示状態の変化を監視し、コールバックを呼び出す React フックです。 +`usePageVisibilityEffect` は、ドキュメントの表示状態の変化を監視し、コールバックを呼び出す React フックです。 +v0 では、このフックは `useVisibilityEvent` という名前でした。以前の名前も引き続き動作しますが、非推奨です。 ## インターフェース ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +44,10 @@ function useVisibilityEvent( ## 使用例 ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`Document is now ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/ko/useVisibilityEvent.md b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/ko/usePageVisibilityEffect.md similarity index 76% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/ko/useVisibilityEvent.md rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/ko/usePageVisibilityEffect.md index c6c13f34..438a0001 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/ko/useVisibilityEvent.md +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/ko/usePageVisibilityEffect.md @@ -1,11 +1,11 @@ -# useVisibilityEvent +# usePageVisibilityEffect -문서의 가시성 상태 변화를 감지하고 콜백을 실행하는 리액트 훅이에요. +문서의 가시성 상태 변화를 감지하고 콜백을 실행하는 리액트 훅이에요. v0에서는 이 훅의 이름이 `useVisibilityEvent`였어요. 이전 이름도 계속 동작하지만 deprecated 상태예요. ## 인터페이스 ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +43,10 @@ function useVisibilityEvent( ## 예시 ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`Document is now ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.md b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.md similarity index 71% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.md rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.md index 52115cc8..24882458 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.md +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.md @@ -1,11 +1,12 @@ -# useVisibilityEvent +# usePageVisibilityEffect -`useVisibilityEvent` is a React hook that listens to changes in the document's visibility state and triggers a callback. +`usePageVisibilityEffect` is a React hook that listens to changes in the document's visibility state and triggers a callback. +In v0 this hook was named `useVisibilityEvent`. The old name still works but is deprecated. ## Interface ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +44,10 @@ This function does not return anything. ## Example ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`Document is now ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.spec.ts b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.test.ts similarity index 70% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.spec.ts rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.test.ts index e2366f40..18d64fa8 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.spec.ts +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.test.ts @@ -3,13 +3,14 @@ import { describe, expect, it, vi } from 'vitest'; import { renderHookSSR } from '../../_internal/test-utils/renderHookSSR.tsx'; +import { usePageVisibilityEffect } from './usePageVisibilityEffect.ts'; import { useVisibilityEvent } from './useVisibilityEvent.ts'; -describe('useVisibilityEvent', () => { +describe('usePageVisibilityEffect', () => { it('calls the callback when visibility changes', async () => { const mockCallback = vi.fn(); - await renderHookSSR(() => useVisibilityEvent(mockCallback)); + await renderHookSSR(() => usePageVisibilityEffect(mockCallback)); await act(async () => simulateVisibilityChange('hidden')); @@ -22,7 +23,7 @@ describe('useVisibilityEvent', () => { it('is safe on server side rendering', () => { const callback = vi.fn(); - renderHookSSR.serverOnly(() => useVisibilityEvent(callback)); + renderHookSSR.serverOnly(() => usePageVisibilityEffect(callback)); expect(callback).not.toHaveBeenCalled(); }); @@ -30,7 +31,7 @@ describe('useVisibilityEvent', () => { it('should not call the callback on initial render if immediate is false', async () => { const mockCallback = vi.fn(); - await renderHookSSR(() => useVisibilityEvent(mockCallback, { immediate: false })); + await renderHookSSR(() => usePageVisibilityEffect(mockCallback, { immediate: false })); expect(mockCallback).not.toHaveBeenCalled(); }); @@ -40,7 +41,7 @@ describe('useVisibilityEvent', () => { await act(async () => simulateVisibilityChange('visible')); - await renderHookSSR(() => useVisibilityEvent(mockCallback, { immediate: true })); + await renderHookSSR(() => usePageVisibilityEffect(mockCallback, { immediate: true })); expect(mockCallback).toHaveBeenCalledWith('visible'); }); @@ -48,7 +49,7 @@ describe('useVisibilityEvent', () => { it('should remove the event listener on unmount', async () => { const mockCallback = vi.fn(); - const { unmount } = await renderHookSSR(() => useVisibilityEvent(mockCallback)); + const { unmount } = await renderHookSSR(() => usePageVisibilityEffect(mockCallback)); unmount(); @@ -56,6 +57,10 @@ describe('useVisibilityEvent', () => { expect(mockCallback).not.toHaveBeenCalled(); }); + + it('keeps the deprecated useVisibilityEvent as the same function', () => { + expect(useVisibilityEvent).toBe(usePageVisibilityEffect); + }); }); function simulateVisibilityChange(visibilityState: 'visible' | 'hidden') { diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.ts b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.ts similarity index 73% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.ts rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.ts index 66bd33b9..b6a5df8a 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/useVisibilityEvent.ts +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/usePageVisibilityEffect.ts @@ -6,7 +6,8 @@ type Options = { /** * @description - * `useVisibilityEvent` is a React hook that listens to changes in the document's visibility state and triggers a callback. + * `usePageVisibilityEffect` is a React hook that listens to changes in the document's visibility state and triggers a callback. + * In v0 this hook was named `useVisibilityEvent`. The old name still works but is deprecated. * * @see `usePageVisibility` returns the visibility as state instead of running a callback. * @@ -17,10 +18,10 @@ type Options = { * with the current visibility state. * * @example - * import { useVisibilityEvent } from 'react-simplikit'; + * import { usePageVisibilityEffect } from 'react-simplikit'; * * function Component() { - * useVisibilityEvent(visibilityState => { + * usePageVisibilityEffect(visibilityState => { * console.log(`Document is now ${visibilityState}`); * }); * @@ -28,7 +29,10 @@ type Options = { * } */ -export function useVisibilityEvent(callback: (visibilityState: 'visible' | 'hidden') => void, options: Options = {}) { +export function usePageVisibilityEffect( + callback: (visibilityState: 'visible' | 'hidden') => void, + options: Options = {} +) { const handleVisibilityChange = useCallback(() => { callback(document.visibilityState); }, [callback]); diff --git a/packages/react-simplikit/src/hooks/usePageVisibilityEffect/useVisibilityEvent.ts b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/useVisibilityEvent.ts new file mode 100644 index 00000000..0bfdb426 --- /dev/null +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/useVisibilityEvent.ts @@ -0,0 +1,9 @@ +import { usePageVisibilityEffect } from './usePageVisibilityEffect.ts'; + +/** + * @description + * `useVisibilityEvent` is the name `usePageVisibilityEffect` had in v0. + * + * @deprecated Use `usePageVisibilityEffect` instead. + */ +export const useVisibilityEvent = usePageVisibilityEffect; diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/zh-Hans/useVisibilityEvent.md b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/zh-Hans/usePageVisibilityEffect.md similarity index 71% rename from packages/react-simplikit/src/hooks/useVisibilityEvent/zh-Hans/useVisibilityEvent.md rename to packages/react-simplikit/src/hooks/usePageVisibilityEffect/zh-Hans/usePageVisibilityEffect.md index fdd5071f..012e5ef4 100644 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/zh-Hans/useVisibilityEvent.md +++ b/packages/react-simplikit/src/hooks/usePageVisibilityEffect/zh-Hans/usePageVisibilityEffect.md @@ -1,11 +1,11 @@ -# useVisibilityEvent +# usePageVisibilityEffect -`useVisibilityEvent` 是一个监听文档可见性状态变化并触发回调的 React Hook。 +`usePageVisibilityEffect` 是一个监听文档可见性状态变化并触发回调的 React Hook。在 v0 中,这个 Hook 名为 `useVisibilityEvent`。旧名称仍然可用,但已弃用。 ## 接口 ```ts -function useVisibilityEvent( +function usePageVisibilityEffect( callback: (visibilityState: 'visible' | 'hidden') => void, options?: object ): void; @@ -43,10 +43,10 @@ function useVisibilityEvent( ## 示例 ```tsx -import { useVisibilityEvent } from 'react-simplikit'; +import { usePageVisibilityEffect } from 'react-simplikit'; function Component() { - useVisibilityEvent(visibilityState => { + usePageVisibilityEffect(visibilityState => { console.log(`Document is now ${visibilityState}`); }); diff --git a/packages/react-simplikit/src/hooks/useVisibilityEvent/index.ts b/packages/react-simplikit/src/hooks/useVisibilityEvent/index.ts deleted file mode 100644 index 3120d82d..00000000 --- a/packages/react-simplikit/src/hooks/useVisibilityEvent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useVisibilityEvent } from './useVisibilityEvent.ts'; diff --git a/packages/react-simplikit/src/index.ts b/packages/react-simplikit/src/index.ts index 99337942..ea9ff934 100644 --- a/packages/react-simplikit/src/index.ts +++ b/packages/react-simplikit/src/index.ts @@ -33,6 +33,7 @@ export { } from './hooks/useNetworkStatus/index.ts'; export { useOutsideClickEffect } from './hooks/useOutsideClickEffect/index.ts'; export { type PageVisibility, usePageVisibility, type VisibilityState } from './hooks/usePageVisibility/index.ts'; +export { usePageVisibilityEffect, useVisibilityEvent } from './hooks/usePageVisibilityEffect/index.ts'; export { usePreservedCallback } from './hooks/usePreservedCallback/index.ts'; export { usePreservedReference } from './hooks/usePreservedReference/index.ts'; export { usePrevious } from './hooks/usePrevious/index.ts'; @@ -46,7 +47,6 @@ export { useThrottledCallback } from './hooks/useThrottledCallback/index.ts'; export { useThrottledValue } from './hooks/useThrottledValue/index.ts'; export { useTimeout } from './hooks/useTimeout/index.ts'; export { useToggle } from './hooks/useToggle/index.ts'; -export { useVisibilityEvent } from './hooks/useVisibilityEvent/index.ts'; export { useVisualViewport } from './hooks/useVisualViewport/index.ts'; export { buildContext } from './utils/buildContext/index.ts'; export { disableBodyScrollLock } from './utils/disableBodyScrollLock/index.ts'; From 78788e2d70b74fa7eeca2bec2eb297ee4027dfbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=A7=80?= <48766355+mnxmnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:13:00 +0900 Subject: [PATCH 06/10] refactor(useBooleanState, useStorageState, useCounter): return the value with an actions object (#532) --- .changeset/state-hook-returns.md | 19 +++++ AGENTS.md | 6 +- CLAUDE.md | 6 +- .../plugin/skills/react-simplikit/SKILL.md | 2 +- .../references/useBooleanState.md | 19 ++--- .../react-simplikit/references/useCounter.md | 14 ++-- .../references/useStorageState.md | 17 ++-- .../useBooleanState/es/useBooleanState.md | 19 ++--- .../useBooleanState/ja/useBooleanState.md | 19 ++--- .../useBooleanState/ko/useBooleanState.md | 19 ++--- .../hooks/useBooleanState/useBooleanState.md | 19 ++--- .../useBooleanState/useBooleanState.spec.ts | 18 ++++- .../hooks/useBooleanState/useBooleanState.ts | 26 ++++--- .../zh-Hans/useBooleanState.md | 19 ++--- .../src/hooks/useCounter/es/useCounter.md | 14 ++-- .../src/hooks/useCounter/ja/useCounter.md | 14 ++-- .../src/hooks/useCounter/ko/useCounter.md | 14 ++-- .../src/hooks/useCounter/useCounter.md | 14 ++-- .../src/hooks/useCounter/useCounter.spec.ts | 78 +++++++++++-------- .../src/hooks/useCounter/useCounter.ts | 32 ++++---- .../hooks/useCounter/zh-Hans/useCounter.md | 14 ++-- .../useStorageState/es/useStorageState.md | 15 ++-- .../useStorageState/ja/useStorageState.md | 15 ++-- .../useStorageState/ko/useStorageState.md | 17 ++-- .../hooks/useStorageState/useStorageState.md | 17 ++-- .../useStorageState/useStorageState.spec.ts | 30 ++++--- .../hooks/useStorageState/useStorageState.ts | 50 ++++++------ .../zh-Hans/useStorageState.md | 17 ++-- .../src/hooks/useToggle/useToggle.ts | 2 +- 29 files changed, 295 insertions(+), 270 deletions(-) create mode 100644 .changeset/state-hook-returns.md diff --git a/.changeset/state-hook-returns.md b/.changeset/state-hook-returns.md new file mode 100644 index 00000000..3062b7c2 --- /dev/null +++ b/.changeset/state-hook-returns.md @@ -0,0 +1,19 @@ +--- +'react-simplikit': major +--- + +`useBooleanState`, `useStorageState` and `useCounter` now return `[value, actions]`, matching `useList`, `useMap` and `useSet`. The actions object keeps the same reference across renders. + +```ts +// before +const [isOpen, setTrue, setFalse, toggle] = useBooleanState(false); +const [token, setToken, refreshToken] = useStorageState('token'); +const { count, increment, decrement, reset, setCount } = useCounter(0); + +// after +const [isOpen, { setTrue, setFalse, toggle }] = useBooleanState(false); +const [token, { setValue, refresh }] = useStorageState('token'); +const [count, { increment, decrement, reset, setCount }] = useCounter(0); +``` + +`useToggle` keeps its `[value, toggle]` signature and now declares its return type, so React 18 consumers no longer see the React 19 `ActionDispatch` type. diff --git a/AGENTS.md b/AGENTS.md index 4e92374c..119d49b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,9 +116,9 @@ Never initialize state with browser API calls (causes hydration mismatch). ### Hook Return Value Convention - **Single value**: `useDebounce(value, delay): T` -- **Tuple** (2 items): `useToggle(init): [boolean, () => void]` -- **Object** (3+ items): `usePagination(): { page, nextPage, prevPage }` -- **Object** also when the shape is expected to grow: `useKeyboardHeight(): { keyboardHeight }` +- **Tuple** (state + one action): `useToggle(init): [boolean, () => void]` +- **Tuple with an actions object** (state + two or more actions): `useBooleanState(init): [boolean, { setTrue, setFalse, toggle }]`, `useList`, `useCounter` +- **Object** for results that are not state + actions, and whenever the shape is expected to grow: browser measurements such as `useKeyboardHeight(): { keyboardHeight }` ## Testing diff --git a/CLAUDE.md b/CLAUDE.md index 2809c868..7332c8fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,9 +89,9 @@ Never initialize state with browser API calls (causes hydration mismatch). ### Hook Return Value Convention - **Single value**: `useDebounce(value, delay): T` -- **Tuple** (state + action, 2 items): `useToggle(init): [boolean, () => void]` -- **Object** (3+ items): `usePagination(): { page, nextPage, prevPage }` -- **Object** also when the shape is expected to grow: browser measurements such as `useKeyboardHeight(): { keyboardHeight }` +- **Tuple** (state + one action): `useToggle(init): [boolean, () => void]` +- **Tuple with an actions object** (state + two or more actions): `useBooleanState(init): [boolean, { setTrue, setFalse, toggle }]`, `useList`, `useCounter` +- **Object** for results that are not state + actions, and whenever the shape is expected to grow: browser measurements such as `useKeyboardHeight(): { keyboardHeight }` ### Performance Patterns diff --git a/packages/plugin/skills/react-simplikit/SKILL.md b/packages/plugin/skills/react-simplikit/SKILL.md index 28292be3..cdcae987 100644 --- a/packages/plugin/skills/react-simplikit/SKILL.md +++ b/packages/plugin/skills/react-simplikit/SKILL.md @@ -107,7 +107,7 @@ Backticks in this table mark catalog entries only. | [`useSafeAreaInset`](references/useSafeAreaInset.md) | `useSafeAreaInset` is a React hook that tracks safe area inset changes. | | [`useScrollDirection`](references/useScrollDirection.md) | `useScrollDirection` is a React hook that detects scroll direction. | | [`useSet`](references/useSet.md) | A React hook that manages a Set as state. | -| [`useStorageState`](references/useStorageState.md) | `useStorageState` is a React that functions like `useState` but persists the state value in browser storage. | +| [`useStorageState`](references/useStorageState.md) | `useStorageState` is a React hook that functions like `useState` but persists the state value in browser storage. | | [`useThrottle`](references/useThrottle.md) | `useThrottle` is a React hook that creates a throttled version of a callback function. | | [`useThrottledCallback`](references/useThrottledCallback.md) | `useThrottledCallback` is a React hook that returns a throttled version of the provided callback function. | | [`useThrottledValue`](references/useThrottledValue.md) | `useThrottledValue` is a React hook that returns a throttled copy of the given value. | diff --git a/packages/plugin/skills/react-simplikit/references/useBooleanState.md b/packages/plugin/skills/react-simplikit/references/useBooleanState.md index 3ef27c3e..50443689 100644 --- a/packages/plugin/skills/react-simplikit/references/useBooleanState.md +++ b/packages/plugin/skills/react-simplikit/references/useBooleanState.md @@ -8,12 +8,7 @@ It provides functions to set the state to `true`, set it to `false`, and toggle ```ts function useBooleanState( initialValue: boolean | (() => boolean) = false -): readonly [ - state: boolean, - setTrue: () => void, - setFalse: () => void, - toggle: () => void, -]; +): readonly [state: boolean, actions: BooleanStateActions]; ``` ### Parameters @@ -28,8 +23,8 @@ function useBooleanState( @@ -97,7 +97,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/plugin/skills/react-simplikit/references/useStorageState.md b/packages/plugin/skills/react-simplikit/references/useStorageState.md index 08ee4a2c..851d5d84 100644 --- a/packages/plugin/skills/react-simplikit/references/useStorageState.md +++ b/packages/plugin/skills/react-simplikit/references/useStorageState.md @@ -1,6 +1,6 @@ # useStorageState -`useStorageState` is a React that functions like `useState` but persists the state value in browser storage. +`useStorageState` is a React hook that functions like `useState` but persists the state value in browser storage. The value is retained across page reloads and can be shared between tabs when using `localStorage`. ## Interface @@ -11,8 +11,7 @@ function useStorageState( options?: Object ): readonly [ state: Serializable | undefined, - setState: (value: SetStateAction | undefined>) => void, - refreshState: () => void, + actions: StorageStateActions | undefined>, ]; ``` @@ -63,8 +62,8 @@ function useStorageState( @@ -97,7 +97,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/react-simplikit/src/hooks/useCounter/ja/useCounter.md b/packages/react-simplikit/src/hooks/useCounter/ja/useCounter.md index 55843629..3ed3033e 100644 --- a/packages/react-simplikit/src/hooks/useCounter/ja/useCounter.md +++ b/packages/react-simplikit/src/hooks/useCounter/ja/useCounter.md @@ -55,7 +55,7 @@ function useCounter( @@ -97,7 +97,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/react-simplikit/src/hooks/useCounter/ko/useCounter.md b/packages/react-simplikit/src/hooks/useCounter/ko/useCounter.md index d176a0ec..f3883246 100644 --- a/packages/react-simplikit/src/hooks/useCounter/ko/useCounter.md +++ b/packages/react-simplikit/src/hooks/useCounter/ko/useCounter.md @@ -54,7 +54,7 @@ function useCounter( @@ -96,7 +96,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/react-simplikit/src/hooks/useCounter/useCounter.md b/packages/react-simplikit/src/hooks/useCounter/useCounter.md index fcfa9049..51f8edd9 100644 --- a/packages/react-simplikit/src/hooks/useCounter/useCounter.md +++ b/packages/react-simplikit/src/hooks/useCounter/useCounter.md @@ -55,7 +55,7 @@ function useCounter( @@ -97,7 +97,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/react-simplikit/src/hooks/useCounter/useCounter.spec.ts b/packages/react-simplikit/src/hooks/useCounter/useCounter.spec.ts index 92784749..950033b0 100644 --- a/packages/react-simplikit/src/hooks/useCounter/useCounter.spec.ts +++ b/packages/react-simplikit/src/hooks/useCounter/useCounter.spec.ts @@ -7,50 +7,50 @@ describe('useCounter', () => { it('should initialize with default value', () => { const { result } = renderHook(() => useCounter()); - expect(result.current.count).toBe(0); + expect(result.current[0]).toBe(0); }); it('should initialize with provided initial value', () => { const { result } = renderHook(() => useCounter(10)); - expect(result.current.count).toBe(10); + expect(result.current[0]).toBe(10); }); it('should increment the counter', async () => { const { result } = renderHook(() => useCounter(5)); await act(async () => { - result.current.increment(); + result.current[1].increment(); }); - expect(result.current.count).toBe(6); + expect(result.current[0]).toBe(6); }); it('should decrement the counter', async () => { const { result } = renderHook(() => useCounter(5)); await act(async () => { - result.current.decrement(); + result.current[1].decrement(); }); - expect(result.current.count).toBe(4); + expect(result.current[0]).toBe(4); }); it('should reset the counter to initial value', async () => { const { result } = renderHook(() => useCounter(5)); await act(async () => { - result.current.increment(); - result.current.increment(); + result.current[1].increment(); + result.current[1].increment(); }); - expect(result.current.count).toBe(7); + expect(result.current[0]).toBe(7); await act(async () => { - result.current.reset(); + result.current[1].reset(); }); - expect(result.current.count).toBe(5); + expect(result.current[0]).toBe(5); }); it('should not go below minimum value', async () => { @@ -61,12 +61,12 @@ describe('useCounter', () => { ); await act(async () => { - result.current.decrement(); - result.current.decrement(); - result.current.decrement(); + result.current[1].decrement(); + result.current[1].decrement(); + result.current[1].decrement(); }); - expect(result.current.count).toBe(3); + expect(result.current[0]).toBe(3); }); it('should not go above maximum value', async () => { @@ -77,12 +77,12 @@ describe('useCounter', () => { ); await act(async () => { - result.current.increment(); - result.current.increment(); - result.current.increment(); + result.current[1].increment(); + result.current[1].increment(); + result.current[1].increment(); }); - expect(result.current.count).toBe(7); + expect(result.current[0]).toBe(7); }); it('should use the provided step value for increment and decrement', async () => { @@ -93,16 +93,16 @@ describe('useCounter', () => { ); await act(async () => { - result.current.increment(); + result.current[1].increment(); }); - expect(result.current.count).toBe(7); + expect(result.current[0]).toBe(7); await act(async () => { - result.current.decrement(); + result.current[1].decrement(); }); - expect(result.current.count).toBe(5); + expect(result.current[0]).toBe(5); }); it('should adjust initial value to match constraints', () => { @@ -112,7 +112,7 @@ describe('useCounter', () => { }) ); - expect(result.current.count).toBe(3); + expect(result.current[0]).toBe(3); const { result: result2 } = renderHook(() => useCounter(10, { @@ -120,7 +120,7 @@ describe('useCounter', () => { }) ); - expect(result2.current.count).toBe(8); + expect(result2.current[0]).toBe(8); }); it('should allow setting arbitrary value within constraints', async () => { @@ -132,31 +132,43 @@ describe('useCounter', () => { ); await act(async () => { - result.current.setCount(6); + result.current[1].setCount(6); }); - expect(result.current.count).toBe(6); + expect(result.current[0]).toBe(6); await act(async () => { - result.current.setCount(1); + result.current[1].setCount(1); }); - expect(result.current.count).toBe(3); + expect(result.current[0]).toBe(3); await act(async () => { - result.current.setCount(10); + result.current[1].setCount(10); }); - expect(result.current.count).toBe(8); + expect(result.current[0]).toBe(8); }); it('should work with updater function for setCount', async () => { const { result } = renderHook(() => useCounter(5)); await act(async () => { - result.current.setCount(prev => prev + 3); + result.current[1].setCount(prev => prev + 3); }); - expect(result.current.count).toBe(8); + expect(result.current[0]).toBe(8); + }); + + it('should keep the actions object stable across renders', () => { + const { result } = renderHook(() => useCounter(0)); + const [, actions] = result.current; + + act(() => { + actions.increment(); + }); + + expect(result.current[0]).toBe(1); + expect(result.current[1]).toBe(actions); }); }); diff --git a/packages/react-simplikit/src/hooks/useCounter/useCounter.ts b/packages/react-simplikit/src/hooks/useCounter/useCounter.ts index 12af9125..0568ec1e 100644 --- a/packages/react-simplikit/src/hooks/useCounter/useCounter.ts +++ b/packages/react-simplikit/src/hooks/useCounter/useCounter.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; type UseCounterOptions = { min?: number; @@ -6,14 +6,15 @@ type UseCounterOptions = { step?: number; }; -type UseCounterReturn = { - count: number; +type CounterActions = { increment: () => void; decrement: () => void; reset: () => void; setCount: (value: number | ((prev: number) => number)) => void; }; +type UseCounterReturn = readonly [number, CounterActions]; + const validateValue = (value: number, { min, max }: Omit): number => { if (min !== undefined && value < min) { return min; @@ -37,18 +38,18 @@ const validateValue = (value: number, { min, max }: Omit void` - A function to increment the count; - * - decrement `() => void` - A function to decrement the count; - * - reset `() => void` - A function to reset the count to the initial value; - * - setCount `(value: number | ((prev: number) => number)) => void` - A function to set the count to a specific value or a function that returns a new value; + * - actions.increment `() => void` - A function to increment the count; + * - actions.decrement `() => void` - A function to decrement the count; + * - actions.reset `() => void` - A function to reset the count to the initial value; + * - actions.setCount `(value: number | ((prev: number) => number)) => void` - A function to set the count, either to a value or with an updater function that receives the previous count and returns the next one; * * @example * import { useCounter } from 'react-simplikit'; * * function ShoppingCart() { - * const { count, increment, decrement, reset } = useCounter(1, { + * const [count, { increment, decrement, reset }] = useCounter(1, { * min: 1, * max: 10, * }); @@ -89,11 +90,10 @@ export function useCounter(initialValue = 0, { min, max, step = 1 }: UseCounterO setCount(initialValue); }, [setCount, initialValue]); - return { - count, - increment, - decrement, - reset, - setCount, - }; + const actions = useMemo( + () => ({ increment, decrement, reset, setCount }), + [increment, decrement, reset, setCount] + ); + + return [count, actions] as const; } diff --git a/packages/react-simplikit/src/hooks/useCounter/zh-Hans/useCounter.md b/packages/react-simplikit/src/hooks/useCounter/zh-Hans/useCounter.md index 6c1fe3f3..dda11d5c 100644 --- a/packages/react-simplikit/src/hooks/useCounter/zh-Hans/useCounter.md +++ b/packages/react-simplikit/src/hooks/useCounter/zh-Hans/useCounter.md @@ -54,7 +54,7 @@ function useCounter( @@ -96,7 +96,7 @@ function useCounter( import { useCounter } from 'react-simplikit'; function ShoppingCart() { - const { count, increment, decrement, reset } = useCounter(1, { + const [count, { increment, decrement, reset }] = useCounter(1, { min: 1, max: 10, }); diff --git a/packages/react-simplikit/src/hooks/useStorageState/es/useStorageState.md b/packages/react-simplikit/src/hooks/useStorageState/es/useStorageState.md index a10a3cc7..f6c11570 100644 --- a/packages/react-simplikit/src/hooks/useStorageState/es/useStorageState.md +++ b/packages/react-simplikit/src/hooks/useStorageState/es/useStorageState.md @@ -11,8 +11,7 @@ function useStorageState( options?: Object ): readonly [ state: Serializable | undefined, - setState: (value: SetStateAction | undefined>) => void, - refreshState: () => void, + actions: StorageStateActions | undefined>, ]; ``` @@ -63,8 +62,8 @@ function useStorageState( - - - ### Return Value @@ -38,13 +48,15 @@ import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('Searching for products...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('Searching for products...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('Almost done...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('Almost done...'), + delayMs: 4000, + }); return
{title}
; } diff --git a/packages/react-simplikit/src/hooks/useInterval/es/useInterval.md b/packages/react-simplikit/src/hooks/useInterval/es/useInterval.md index 00ecbc3c..b1c30d2d 100644 --- a/packages/react-simplikit/src/hooks/useInterval/es/useInterval.md +++ b/packages/react-simplikit/src/hooks/useInterval/es/useInterval.md @@ -6,33 +6,28 @@ Es útil para temporizadores, consultas periódicas de datos y otras tareas recu ## Interfaz ```ts -function useInterval( - callback: () => void, - options: number | { delay: number; enabled?: boolean; immediate?: boolean } -): void; +function useInterval(options: Object): void; ``` ### Parámetros - - - - - - - - ### Valor de retorno @@ -38,13 +47,15 @@ import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('Buscando productos...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('Buscando productos...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('Casi listo...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('Casi listo...'), + delayMs: 4000, + }); return
{title}
; } diff --git a/packages/react-simplikit/src/hooks/useTimeout/ja/useTimeout.md b/packages/react-simplikit/src/hooks/useTimeout/ja/useTimeout.md index 8b065c34..2fdfe65c 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/ja/useTimeout.md +++ b/packages/react-simplikit/src/hooks/useTimeout/ja/useTimeout.md @@ -6,22 +6,31 @@ React のライフサイクルに合わせて `setTimeout` を管理し、アン ## インターフェース ```ts -function useTimeout(callback: () => void, delay: number = 0): void; +function useTimeout(options: Object): void; ``` ### パラメータ - - ### 戻り値 @@ -38,13 +47,15 @@ import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('Searching for products...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('Searching for products...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('Almost done...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('Almost done...'), + delayMs: 4000, + }); return
{title}
; } diff --git a/packages/react-simplikit/src/hooks/useTimeout/ko/useTimeout.md b/packages/react-simplikit/src/hooks/useTimeout/ko/useTimeout.md index 89c660e2..38db96f2 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/ko/useTimeout.md +++ b/packages/react-simplikit/src/hooks/useTimeout/ko/useTimeout.md @@ -5,22 +5,31 @@ ## 인터페이스 ```ts -function useTimeout(callback: () => void, delay: number = 0): void; +function useTimeout(options: Object): void; ``` ### 파라미터 - - ### 반환 값 @@ -37,13 +46,15 @@ import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('제품을 검색 중이에요...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('제품을 검색 중이에요...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('거의 완료됐어요...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('거의 완료됐어요...'), + delayMs: 4000, + }); return
{title}
; } diff --git a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.md b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.md index c2cd451d..a0f79491 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.md +++ b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.md @@ -6,22 +6,32 @@ It manages `setTimeout` in accordance with the React lifecycle, ensuring cleanup ## Interface ```ts -function useTimeout(callback: () => void, delay: number = 0): void; +function useTimeout(options: Object): void; ``` ### Parameters - - ### Return Value @@ -38,13 +48,15 @@ import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('Searching for products...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('Searching for products...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('Almost done...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('Almost done...'), + delayMs: 4000, + }); return
{title}
; } diff --git a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.spec.ts b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.spec.ts index e51a486e..8ae2a6fe 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.spec.ts +++ b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.spec.ts @@ -15,14 +15,14 @@ describe('useTimeout', () => { it('is safe on server side rendering', () => { const callback = vi.fn(); - renderHookSSR.serverOnly(() => useTimeout(callback, 1000)); + renderHookSSR.serverOnly(() => useTimeout({ onTimeout: callback, delayMs: 1000 })); expect(callback).not.toHaveBeenCalled(); }); it('should call callback after specified delay', async () => { const callback = vi.fn(); - await renderHookSSR(() => useTimeout(callback, 1000)); + await renderHookSSR(() => useTimeout({ onTimeout: callback, delayMs: 1000 })); expect(callback).not.toHaveBeenCalled(); @@ -32,7 +32,7 @@ describe('useTimeout', () => { it('should use 0ms as default delay', async () => { const callback = vi.fn(); - await renderHookSSR(() => useTimeout(callback)); + await renderHookSSR(() => useTimeout({ onTimeout: callback })); expect(callback).not.toHaveBeenCalled(); @@ -42,7 +42,7 @@ describe('useTimeout', () => { it('should clear timeout on unmount', async () => { const callback = vi.fn(); - const { unmount } = await renderHookSSR(() => useTimeout(callback, 1000)); + const { unmount } = await renderHookSSR(() => useTimeout({ onTimeout: callback, delayMs: 1000 })); unmount(); vi.advanceTimersByTime(1000); @@ -52,7 +52,7 @@ describe('useTimeout', () => { it('should keep the timeout when rerendering happens', async () => { const callback = vi.fn(); - const { rerender } = await renderHookSSR(() => useTimeout(callback, 3000)); + const { rerender } = await renderHookSSR(() => useTimeout({ onTimeout: callback, delayMs: 3000 })); vi.advanceTimersByTime(1500); rerender(); @@ -64,7 +64,7 @@ describe('useTimeout', () => { it('should reset timeout when delay changes', async () => { const callback = vi.fn(); - const { rerender } = await renderHookSSR(({ delay }) => useTimeout(callback, delay), { + const { rerender } = await renderHookSSR(({ delay }) => useTimeout({ onTimeout: callback, delayMs: delay }), { initialProps: { delay: 1000 }, }); @@ -84,8 +84,8 @@ describe('useTimeout', () => { const callback2 = vi.fn(); await renderHookSSR(() => { - useTimeout(callback1, 1000); - useTimeout(callback2, 2000); + useTimeout({ onTimeout: callback1, delayMs: 1000 }); + useTimeout({ onTimeout: callback2, delayMs: 2000 }); }); vi.advanceTimersByTime(1000); @@ -98,7 +98,7 @@ describe('useTimeout', () => { it('should treat negative delay as 0ms', async () => { const callback = vi.fn(); - await renderHookSSR(() => useTimeout(callback, -1000)); + await renderHookSSR(() => useTimeout({ onTimeout: callback, delayMs: -1000 })); expect(callback).not.toHaveBeenCalled(); vi.advanceTimersByTime(0); @@ -107,7 +107,7 @@ describe('useTimeout', () => { it('should treat undefined delay as 0ms', async () => { const callback = vi.fn(); - await renderHookSSR(() => useTimeout(callback, undefined)); + await renderHookSSR(() => useTimeout({ onTimeout: callback, delayMs: undefined })); vi.advanceTimersByTime(0); expect(callback).toHaveBeenCalledTimes(1); diff --git a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.ts b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.ts index ee06d93a..90b7d944 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/useTimeout.ts +++ b/packages/react-simplikit/src/hooks/useTimeout/useTimeout.ts @@ -2,13 +2,19 @@ import { useEffect } from 'react'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; +type UseTimeoutOptions = { + onTimeout: () => void; + delayMs?: number; +}; + /** * @description * `useTimeout` is a React hook that executes a callback function after a specified delay. * It manages `setTimeout` in accordance with the React lifecycle, ensuring cleanup on unmount or when dependencies change. * - * @param {() => void} callback - The function to be executed after the delay. - * @param {number} [delay=0] - The time in milliseconds to wait before executing the callback. + * @param {Object} options - Configures the timeout behavior. + * @param {() => void} options.onTimeout - The function to be executed after the delay. + * @param {number} [options.delayMs=0] - The time in milliseconds to wait before executing `onTimeout`. * * @example * // Updating a title after a delay @@ -18,23 +24,27 @@ import { usePreservedCallback } from '../usePreservedCallback/index.ts'; * function Example() { * const [title, setTitle] = useState(''); * - * useTimeout(() => { - * setTitle('Searching for products...'); - * }, 2000); + * useTimeout({ + * onTimeout: () => setTitle('Searching for products...'), + * delayMs: 2000, + * }); * - * useTimeout(() => { - * setTitle('Almost done...'); - * }, 4000); + * useTimeout({ + * onTimeout: () => setTitle('Almost done...'), + * delayMs: 4000, + * }); * * return
{title}
; * } */ +export function useTimeout({ onTimeout, delayMs = 0 }: UseTimeoutOptions) { + const preservedCallback = usePreservedCallback(onTimeout); -export function useTimeout(callback: () => void, delay = 0) { - const preservedCallback = usePreservedCallback(callback); - - useEffect(() => { - const timeoutId = setTimeout(preservedCallback, delay); - return () => clearTimeout(timeoutId); - }, [delay, preservedCallback]); + useEffect( + function startTimeout() { + const timeoutId = setTimeout(preservedCallback, delayMs); + return () => clearTimeout(timeoutId); + }, + [delayMs, preservedCallback] + ); } diff --git a/packages/react-simplikit/src/hooks/useTimeout/zh-Hans/useTimeout.md b/packages/react-simplikit/src/hooks/useTimeout/zh-Hans/useTimeout.md index acd29093..b2d12741 100644 --- a/packages/react-simplikit/src/hooks/useTimeout/zh-Hans/useTimeout.md +++ b/packages/react-simplikit/src/hooks/useTimeout/zh-Hans/useTimeout.md @@ -5,22 +5,31 @@ ## 接口 ```ts -function useTimeout(callback: () => void, delay: number = 0): void; +function useTimeout(options: Object): void; ``` ### 参数 - - ### 返回值 @@ -30,20 +39,22 @@ function useTimeout(callback: () => void, delay: number = 0): void; ## 示例 ```tsx -// Updating a title after a delay +// 延迟后更新标题 import { useTimeout } from 'react-simplikit'; import { useState } from 'react'; function Example() { const [title, setTitle] = useState(''); - useTimeout(() => { - setTitle('Searching for products...'); - }, 2000); + useTimeout({ + onTimeout: () => setTitle('Searching for products...'), + delayMs: 2000, + }); - useTimeout(() => { - setTitle('Almost done...'); - }, 4000); + useTimeout({ + onTimeout: () => setTitle('Almost done...'), + delayMs: 4000, + }); return
{title}
; } From d5a46be97000eaa8612c3660f5aa966aacbb6fd6 Mon Sep 17 00:00:00 2001 From: SiHyunLee Date: Thu, 8 Oct 2026 17:29:01 +0900 Subject: [PATCH 08/10] refactor: make millisecond units explicit in time parameter names (#552) --- .changeset/deep-pandas-stare.md | 21 +++++++++++++ docs/es/use-cases.md | 2 +- docs/ja/use-cases.md | 2 +- docs/ko/use-cases.md | 2 +- docs/use-cases.md | 2 +- docs/zh-Hans/use-cases.md | 2 +- .../references/ImpressionArea.md | 6 ++-- .../references/useAvoidKeyboard.md | 2 +- .../react-simplikit/references/useDebounce.md | 8 ++--- .../references/useDebouncedCallback.md | 4 +-- .../references/useDebouncedValue.md | 10 +++---- .../references/useImpressionRef.md | 4 +-- .../references/useLongPress.md | 4 +-- .../react-simplikit/references/useThrottle.md | 4 +-- .../references/useThrottledCallback.md | 4 +-- .../references/useThrottledValue.md | 10 +++---- .../ImpressionArea/ImpressionArea.md | 6 ++-- .../ImpressionArea/ImpressionArea.tsx | 8 ++--- .../ImpressionArea/es/ImpressionArea.md | 6 ++-- .../ImpressionArea/ja/ImpressionArea.md | 6 ++-- .../ImpressionArea/ko/ImpressionArea.md | 6 ++-- .../ImpressionArea/zh-Hans/ImpressionArea.md | 6 ++-- .../useAvoidKeyboard/es/useAvoidKeyboard.md | 2 +- .../useAvoidKeyboard/ja/useAvoidKeyboard.md | 2 +- .../useAvoidKeyboard/ko/useAvoidKeyboard.md | 2 +- .../useAvoidKeyboard/useAvoidKeyboard.md | 2 +- .../useAvoidKeyboard/useAvoidKeyboard.test.ts | 4 +-- .../useAvoidKeyboard/useAvoidKeyboard.ts | 10 +++---- .../zh-Hans/useAvoidKeyboard.md | 2 +- .../src/hooks/useDebounce/es/useDebounce.md | 8 ++--- .../src/hooks/useDebounce/ja/useDebounce.md | 8 ++--- .../src/hooks/useDebounce/ko/useDebounce.md | 6 ++-- .../src/hooks/useDebounce/useDebounce.md | 8 ++--- .../src/hooks/useDebounce/useDebounce.ts | 12 ++++---- .../hooks/useDebounce/zh-Hans/useDebounce.md | 6 ++-- .../es/useDebouncedCallback.md | 4 +-- .../ja/useDebouncedCallback.md | 4 +-- .../ko/useDebouncedCallback.md | 4 +-- .../useDebouncedCallback.md | 4 +-- .../useDebouncedCallback.spec.ts | 20 ++++++------- .../useDebouncedCallback.ts | 10 +++---- .../zh-Hans/useDebouncedCallback.md | 4 +-- .../useDebouncedValue/es/useDebouncedValue.md | 10 +++---- .../useDebouncedValue/ja/useDebouncedValue.md | 10 +++---- .../useDebouncedValue/ko/useDebouncedValue.md | 10 +++---- .../useDebouncedValue/useDebouncedValue.md | 10 +++---- .../useDebouncedValue/useDebouncedValue.ts | 12 ++++---- .../zh-Hans/useDebouncedValue.md | 10 +++---- .../useImpressionRef/es/useImpressionRef.md | 4 +-- .../useImpressionRef/ja/useImpressionRef.md | 4 +-- .../useImpressionRef/ko/useImpressionRef.md | 4 +-- .../useImpressionRef/useImpressionRef.md | 4 +-- .../useImpressionRef/useImpressionRef.spec.ts | 2 +- .../useImpressionRef/useImpressionRef.ts | 10 +++---- .../zh-Hans/useImpressionRef.md | 4 +-- .../src/hooks/useLongPress/es/useLongPress.md | 4 +-- .../src/hooks/useLongPress/ja/useLongPress.md | 6 ++-- .../src/hooks/useLongPress/ko/useLongPress.md | 4 +-- .../src/hooks/useLongPress/useLongPress.md | 4 +-- .../hooks/useLongPress/useLongPress.spec.tsx | 4 +-- .../src/hooks/useLongPress/useLongPress.ts | 12 ++++---- .../useLongPress/zh-Hans/useLongPress.md | 4 +-- .../src/hooks/useThrottle/es/useThrottle.md | 4 +-- .../src/hooks/useThrottle/ja/useThrottle.md | 4 +-- .../src/hooks/useThrottle/ko/useThrottle.md | 4 +-- .../src/hooks/useThrottle/useThrottle.md | 4 +-- .../src/hooks/useThrottle/useThrottle.spec.ts | 4 +-- .../src/hooks/useThrottle/useThrottle.ts | 8 ++--- .../hooks/useThrottle/zh-Hans/useThrottle.md | 4 +-- .../es/useThrottledCallback.md | 4 +-- .../ja/useThrottledCallback.md | 4 +-- .../ko/useThrottledCallback.md | 4 +-- .../useThrottledCallback.md | 4 +-- .../useThrottledCallback.spec.ts | 30 +++++++++---------- .../useThrottledCallback.ts | 10 +++---- .../zh-Hans/useThrottledCallback.md | 4 +-- .../useThrottledValue/es/useThrottledValue.md | 10 +++---- .../useThrottledValue/ja/useThrottledValue.md | 10 +++---- .../useThrottledValue/ko/useThrottledValue.md | 10 +++---- .../useThrottledValue/useThrottledValue.md | 10 +++---- .../useThrottledValue/useThrottledValue.ts | 12 ++++---- .../zh-Hans/useThrottledValue.md | 10 +++---- .../src/hooks/useTimeout/useTimeout.spec.ts | 10 +++---- 83 files changed, 282 insertions(+), 261 deletions(-) create mode 100644 .changeset/deep-pandas-stare.md diff --git a/.changeset/deep-pandas-stare.md b/.changeset/deep-pandas-stare.md new file mode 100644 index 00000000..3988ddad --- /dev/null +++ b/.changeset/deep-pandas-stare.md @@ -0,0 +1,21 @@ +--- +'react-simplikit': major +--- + +Time-related options and parameters now include `Ms` in their names. +These option keys are renamed, so existing calls need to be updated: + +- `useDebouncedCallback`: `timeThreshold` → `debounceMs` +- `useThrottledCallback`: `timeThreshold` → `throttleMs` +- `useImpressionRef`, `ImpressionArea`: `timeThreshold` → `timeThresholdMs` +- `useLongPress`: `delay` → `delayMs` +- `useAvoidKeyboard`: `transitionDuration` → `transitionDurationMs` + +```ts +// before +useDebouncedCallback({ onChange: setQuery, timeThreshold: 300 }); +// after +useDebouncedCallback({ onChange: setQuery, debounceMs: 300 }); +``` + +The positional `wait` parameter of `useDebounce`, `useDebouncedValue`, `useThrottle` and `useThrottledValue` is now `debounceMs` / `throttleMs`. Calls like `useDebounce(fn, 300)` keep working. diff --git a/docs/es/use-cases.md b/docs/es/use-cases.md index e99627e7..76bebfc2 100644 --- a/docs/es/use-cases.md +++ b/docs/es/use-cases.md @@ -84,7 +84,7 @@ Usa [useDebouncedValue](/es/hooks/useDebouncedValue) cuando necesites un valor p - `useStorageState` usa `defaultValue` para la instantánea del servidor y la hidratación, y luego lee el almacenamiento del navegador en el cliente. Al desmontarse, elimina los listeners de almacenamiento. -- `useDebounce` cancela las llamadas pendientes cuando el componente se desmonta o cambian `wait`, `leading` o `trailing`. No cancela una solicitud de red que ya haya comenzado; tu aplicación debe gestionar la cancelación o las respuestas obsoletas. +- `useDebounce` cancela las llamadas pendientes cuando el componente se desmonta o cambian `debounceMs`, `leading` o `trailing`. No cancela una solicitud de red que ya haya comenzado; tu aplicación debe gestionar la cancelación o las respuestas obsoletas. - Las mediciones del navegador pueden cambiar después del montaje. Consulta los valores iniciales y las restricciones de plataforma en [Web móvil](/es/mobile-web) y en la referencia de cada API. diff --git a/docs/ja/use-cases.md b/docs/ja/use-cases.md index 76841681..dc88a871 100644 --- a/docs/ja/use-cases.md +++ b/docs/ja/use-cases.md @@ -84,7 +84,7 @@ export function FruitSearch() { - `useStorageState` はサーバースナップショットと hydration に `defaultValue` を使い、その後クライアントでブラウザストレージを読み取ります。アンマウント時にストレージのリスナーを解除します。 -- `useDebounce` はコンポーネントのアンマウント時、または `wait`、`leading`、`trailing` の変更時に保留中の呼び出しをキャンセルします。開始済みのネットワークリクエストはキャンセルしないため、リクエストの中止や古いレスポンスの処理はアプリケーション側で行う必要があります。 +- `useDebounce` はコンポーネントのアンマウント時、または `debounceMs`、`leading`、`trailing` の変更時に保留中の呼び出しをキャンセルします。開始済みのネットワークリクエストはキャンセルしないため、リクエストの中止や古いレスポンスの処理はアプリケーション側で行う必要があります。 - ブラウザの測定値はマウント後に変わることがあります。初期値とプラットフォームの制約は [モバイル Web](/ja/mobile-web) と各 API のリファレンスで確認してください。 diff --git a/docs/ko/use-cases.md b/docs/ko/use-cases.md index d3440f9c..a48c819b 100644 --- a/docs/ko/use-cases.md +++ b/docs/ko/use-cases.md @@ -84,7 +84,7 @@ export function FruitSearch() { - `useStorageState`는 서버 스냅샷과 hydration에 `defaultValue`를 사용한 뒤 클라이언트에서 브라우저 저장소를 읽어요. 언마운트 시 저장소 리스너를 제거해요. -- `useDebounce`는 컴포넌트가 언마운트되거나 `wait`, `leading`, `trailing`이 바뀌면 대기 중인 호출을 취소해요. 이미 시작한 네트워크 요청은 취소하지 않으므로 요청 취소나 오래된 응답 처리는 애플리케이션에서 담당해야 해요. +- `useDebounce`는 컴포넌트가 언마운트되거나 `debounceMs`, `leading`, `trailing`이 바뀌면 대기 중인 호출을 취소해요. 이미 시작한 네트워크 요청은 취소하지 않으므로 요청 취소나 오래된 응답 처리는 애플리케이션에서 담당해야 해요. - 브라우저 측정값은 마운트 후에 바뀔 수 있어요. 초기값과 플랫폼 제약은 [모바일 웹](/ko/mobile-web) 가이드와 개별 API 레퍼런스에서 확인하세요. diff --git a/docs/use-cases.md b/docs/use-cases.md index 8e69873b..c2bd686e 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -84,7 +84,7 @@ Use [useDebouncedValue](/hooks/useDebouncedValue) when you need a value for rend - `useStorageState` uses `defaultValue` for the server snapshot and hydration, then reads browser storage on the client. Its storage listeners are removed on unmount. -- `useDebounce` cancels pending calls when the component unmounts or when `wait`, `leading`, or `trailing` changes. It does not cancel a network request that has already started; the application must handle request cancellation or stale responses. +- `useDebounce` cancels pending calls when the component unmounts or when `debounceMs`, `leading`, or `trailing` changes. It does not cancel a network request that has already started; the application must handle request cancellation or stale responses. - Browser measurements can change after mount. Check the initial values and platform constraints in [Mobile Web](/mobile-web) and the individual API reference. diff --git a/docs/zh-Hans/use-cases.md b/docs/zh-Hans/use-cases.md index 08782da5..be2e3675 100644 --- a/docs/zh-Hans/use-cases.md +++ b/docs/zh-Hans/use-cases.md @@ -84,7 +84,7 @@ export function FruitSearch() { - `useStorageState` 使用 `defaultValue` 作为服务端快照和 hydration 的值,随后在客户端读取浏览器存储。卸载时会移除存储监听器。 -- `useDebounce` 在组件卸载或 `wait`、`leading`、`trailing` 发生变化时取消待执行的调用,但不会取消已经开始的网络请求。应用需要处理请求取消或过期响应。 +- `useDebounce` 在组件卸载或 `debounceMs`、`leading`、`trailing` 发生变化时取消待执行的调用,但不会取消已经开始的网络请求。应用需要处理请求取消或过期响应。 - 浏览器测量值可能在挂载后发生变化。请在[移动 Web](/zh-Hans/mobile-web) 指南和各 API 参考文档中查看初始值及平台限制。 diff --git a/packages/plugin/skills/react-simplikit/references/ImpressionArea.md b/packages/plugin/skills/react-simplikit/references/ImpressionArea.md index ee15510f..d75ea05e 100644 --- a/packages/plugin/skills/react-simplikit/references/ImpressionArea.md +++ b/packages/plugin/skills/react-simplikit/references/ImpressionArea.md @@ -11,7 +11,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -41,7 +41,7 @@ function ImpressionArea( /> @@ -92,7 +92,7 @@ function App() { console.log('Element entered view')} onImpressionEnd={() => console.log('Element exited view')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
Track me!
diff --git a/packages/plugin/skills/react-simplikit/references/useAvoidKeyboard.md b/packages/plugin/skills/react-simplikit/references/useAvoidKeyboard.md index 9f3cc571..a87b929b 100644 --- a/packages/plugin/skills/react-simplikit/references/useAvoidKeyboard.md +++ b/packages/plugin/skills/react-simplikit/references/useAvoidKeyboard.md @@ -28,7 +28,7 @@ function useAvoidKeyboard( 'Base bottom offset in pixels when the keyboard is hidden. Useful for accounting for the iPhone home indicator area.', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/plugin/skills/react-simplikit/references/useDebounce.md b/packages/plugin/skills/react-simplikit/references/useDebounce.md index 3ece3aab..375b2c0e 100644 --- a/packages/plugin/skills/react-simplikit/references/useDebounce.md +++ b/packages/plugin/skills/react-simplikit/references/useDebounce.md @@ -3,8 +3,8 @@ `useDebounce` is a React hook that returns a debounced version of the provided callback function. It helps optimize event handling by delaying function execution and grouping multiple calls into one. -With the default options, the last call runs after `wait` milliseconds without another call. -Pending calls are cancelled when the component unmounts or when `wait`, `leading`, or `trailing` changes. +With the default options, the last call runs after `debounceMs` milliseconds without another call. +Pending calls are cancelled when the component unmounts or when `debounceMs`, `leading`, or `trailing` changes. Calling `.cancel()` only cancels a pending callback, not an already-started network request. The example displays the query locally. To search a server, pass your application's search callback as the first argument to `useDebounce`. @@ -14,7 +14,7 @@ search callback as the first argument to `useDebounce`. ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -30,7 +30,7 @@ function useDebounce unknown>( diff --git a/packages/plugin/skills/react-simplikit/references/useDebouncedCallback.md b/packages/plugin/skills/react-simplikit/references/useDebouncedCallback.md index 7c70c45e..2dcc8862 100644 --- a/packages/plugin/skills/react-simplikit/references/useDebouncedCallback.md +++ b/packages/plugin/skills/react-simplikit/references/useDebouncedCallback.md @@ -27,7 +27,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; 'The callback to debounce. A call with the same value as the last forwarded one is skipped.', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -70,7 +70,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/plugin/skills/react-simplikit/references/useDebouncedValue.md b/packages/plugin/skills/react-simplikit/references/useDebouncedValue.md index 1d7cbc5c..0cd15500 100644 --- a/packages/plugin/skills/react-simplikit/references/useDebouncedValue.md +++ b/packages/plugin/skills/react-simplikit/references/useDebouncedValue.md @@ -2,7 +2,7 @@ `useDebouncedValue` is a React hook that returns a debounced copy of the given value. The caller keeps owning the state; the hook only delays how quickly the returned value follows it. -The returned value updates `wait` milliseconds after the last change, which is useful for +The returned value updates `debounceMs` milliseconds after the last change, which is useful for deriving a search query or a validation input from fast-changing state. Pending updates are cancelled when the component unmounts. The example displays the @@ -13,7 +13,7 @@ on mount, so with `leading: true` the first change after mount is applied immedi If both `leading` and `trailing` are `false`, the returned value never updates. The value is compared by reference. Passing a new object or array on every render keeps -the returned value updating every `wait` milliseconds; stabilize the reference first, for +the returned value updating every `debounceMs` milliseconds; stabilize the reference first, for example with `usePreservedReference`. ## Interface @@ -21,7 +21,7 @@ example with `usePreservedReference`. ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -37,7 +37,7 @@ function useDebouncedValue( @@ -61,7 +61,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - 'If true, the last change is applied after wait milliseconds.', + 'If true, the last change is applied after debounceMs milliseconds.', }, ]" /> diff --git a/packages/plugin/skills/react-simplikit/references/useImpressionRef.md b/packages/plugin/skills/react-simplikit/references/useImpressionRef.md index 14602f6a..63b7f3c2 100644 --- a/packages/plugin/skills/react-simplikit/references/useImpressionRef.md +++ b/packages/plugin/skills/react-simplikit/references/useImpressionRef.md @@ -33,7 +33,7 @@ function useImpressionRef( description: 'Callback function executed when the element exits the view', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -72,7 +72,7 @@ function Component() { const ref = useImpressionRef({ onImpressionStart: () => console.log('Element entered view'), onImpressionEnd: () => console.log('Element exited view'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/plugin/skills/react-simplikit/references/useLongPress.md b/packages/plugin/skills/react-simplikit/references/useLongPress.md index 4990a515..b4bf1ae7 100644 --- a/packages/plugin/skills/react-simplikit/references/useLongPress.md +++ b/packages/plugin/skills/react-simplikit/references/useLongPress.md @@ -27,7 +27,7 @@ function useLongPress( description="Configuration options for the long press behavior." :nested="[ { - name: 'options.delay', + name: 'options.delayMs', type: 'number', required: false, defaultValue: '500', @@ -131,7 +131,7 @@ function ContextMenu() { const [menuVisible, setMenuVisible] = useState(false); const longPressHandlers = useLongPress(() => setMenuVisible(true), { - delay: 400, + delayMs: 400, onClick: () => console.log('Normal click'), onLongPressEnd: () => console.log('Long press completed'), }); diff --git a/packages/plugin/skills/react-simplikit/references/useThrottle.md b/packages/plugin/skills/react-simplikit/references/useThrottle.md index 94063491..c6d7fdb5 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottle.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottle.md @@ -9,7 +9,7 @@ such as when handling scroll or resize events. ```ts function useThrottle any>( callback: F, - wait: number, + throttleMs: number, options?: ThrottleOptions ): F & { cancel: () => void }; ``` @@ -25,7 +25,7 @@ function useThrottle any>( diff --git a/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md b/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md index 6e0e02b4..a73a19d6 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottledCallback.md @@ -25,7 +25,7 @@ function useThrottledCallback(options: Object): (nextValue: T) => void; 'The callback to throttle. A call with the same value as the last forwarded one is skipped.', }, { - name: 'options.timeThreshold', + name: 'options.throttleMs', type: 'number', required: true, description: 'The number of milliseconds to throttle invocations to.', @@ -67,7 +67,7 @@ function ScrollPosition() { const [scrollTop, setScrollTop] = useState(0); const setScrollTopThrottled = useThrottledCallback({ onChange: setScrollTop, - timeThreshold: 200, + throttleMs: 200, }); return ( diff --git a/packages/plugin/skills/react-simplikit/references/useThrottledValue.md b/packages/plugin/skills/react-simplikit/references/useThrottledValue.md index 87d11e99..0680e8e2 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottledValue.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottledValue.md @@ -1,7 +1,7 @@ # useThrottledValue `useThrottledValue` is a React hook that returns a throttled copy of the given value. -The caller keeps owning the state; the returned value follows it at most once per `wait` milliseconds, +The caller keeps owning the state; the returned value follows it at most once per `throttleMs` milliseconds, which is useful for driving expensive renders from scroll position, pointer position, or an element's size on resize. @@ -10,7 +10,7 @@ on mount, so the first change after mount is applied immediately when `leading` If both `leading` and `trailing` are `false`, the returned value never updates. The value is compared by reference. Passing a new object or array on every render keeps -the returned value updating every `wait` milliseconds; stabilize the reference first, for +the returned value updating every `throttleMs` milliseconds; stabilize the reference first, for example with `usePreservedReference`. ## Interface @@ -18,7 +18,7 @@ example with `usePreservedReference`. ```ts function useThrottledValue( value: T, - wait: number, + throttleMs: number, options?: ThrottleOptions ): T; ``` @@ -34,7 +34,7 @@ function useThrottledValue( @@ -58,7 +58,7 @@ function useThrottledValue( required: false, defaultValue: 'true', description: - 'If true, the last change in a window is applied wait milliseconds after that change.', + 'If true, the last change in a window is applied throttleMs milliseconds after that change.', }, ]" /> diff --git a/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.md b/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.md index ee15510f..d75ea05e 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.md +++ b/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.md @@ -11,7 +11,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -41,7 +41,7 @@ function ImpressionArea( /> @@ -92,7 +92,7 @@ function App() { console.log('Element entered view')} onImpressionEnd={() => console.log('Element exited view')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
Track me!
diff --git a/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.tsx b/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.tsx index ec84b560..44c909ed 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.tsx +++ b/packages/react-simplikit/src/components/ImpressionArea/ImpressionArea.tsx @@ -25,7 +25,7 @@ type Props = React.ComponentPropsWithoutRef & * @param {T} [as='div'] - The HTML tag to render. Defaults to `div`. * @param {string} [rootMargin] - Margin to adjust the detection area. * @param {number} [areaThreshold] - Minimum ratio of the element that must be visible (0 to 1). - * @param {number} [timeThreshold] - Minimum time the element must be visible (in milliseconds). + * @param {number} [timeThresholdMs] - Minimum time the element must be visible (in milliseconds). * @param {() => void} [onImpressionStart] - Callback function executed when the element enters the view. * @param {() => void} [onImpressionEnd] - Callback function executed when the element exits the view. * @param {Ref>} [ref] - Reference to the element. @@ -40,7 +40,7 @@ type Props = React.ComponentPropsWithoutRef & * console.log('Element entered view')} * onImpressionEnd={() => console.log('Element exited view')} - * timeThreshold={1000} + * timeThresholdMs={1000} * areaThreshold={0.5} * > *
Track me!
@@ -53,7 +53,7 @@ export const ImpressionArea = forwardRef(ImpressionAreaImpl) as React.ReactElement; function ImpressionAreaImpl( - { as, rootMargin, areaThreshold, timeThreshold, onImpressionStart, onImpressionEnd, ...props }: Props, + { as, rootMargin, areaThreshold, timeThresholdMs, onImpressionStart, onImpressionEnd, ...props }: Props, ref: Ref ) { const Component = as ?? 'div'; @@ -61,7 +61,7 @@ function ImpressionAreaImpl( onImpressionStart, onImpressionEnd, areaThreshold, - timeThreshold, + timeThresholdMs, rootMargin, }); diff --git a/packages/react-simplikit/src/components/ImpressionArea/es/ImpressionArea.md b/packages/react-simplikit/src/components/ImpressionArea/es/ImpressionArea.md index b94c9d0d..27bc468f 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/es/ImpressionArea.md +++ b/packages/react-simplikit/src/components/ImpressionArea/es/ImpressionArea.md @@ -11,7 +11,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -41,7 +41,7 @@ function ImpressionArea( /> @@ -94,7 +94,7 @@ function App() { console.log('El elemento entró en el área visible') } onImpressionEnd={() => console.log('El elemento salió del área visible')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
¡Sigue mi visibilidad!
diff --git a/packages/react-simplikit/src/components/ImpressionArea/ja/ImpressionArea.md b/packages/react-simplikit/src/components/ImpressionArea/ja/ImpressionArea.md index fcc1cab5..7f70ce9b 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/ja/ImpressionArea.md +++ b/packages/react-simplikit/src/components/ImpressionArea/ja/ImpressionArea.md @@ -11,7 +11,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -41,7 +41,7 @@ function ImpressionArea( /> @@ -92,7 +92,7 @@ function App() { console.log('要素が表示領域に入りました')} onImpressionEnd={() => console.log('要素が表示領域から出ました')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
この要素を追跡してください!
diff --git a/packages/react-simplikit/src/components/ImpressionArea/ko/ImpressionArea.md b/packages/react-simplikit/src/components/ImpressionArea/ko/ImpressionArea.md index 3948cf10..ad61e5e0 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/ko/ImpressionArea.md +++ b/packages/react-simplikit/src/components/ImpressionArea/ko/ImpressionArea.md @@ -9,7 +9,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -39,7 +39,7 @@ function ImpressionArea( /> @@ -90,7 +90,7 @@ function App() { console.log('요소가 보여요')} onImpressionEnd={() => console.log('요소가 사라졌어요')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
저를 추적해 보세요!
diff --git a/packages/react-simplikit/src/components/ImpressionArea/zh-Hans/ImpressionArea.md b/packages/react-simplikit/src/components/ImpressionArea/zh-Hans/ImpressionArea.md index 66fb08f0..cbb10343 100644 --- a/packages/react-simplikit/src/components/ImpressionArea/zh-Hans/ImpressionArea.md +++ b/packages/react-simplikit/src/components/ImpressionArea/zh-Hans/ImpressionArea.md @@ -9,7 +9,7 @@ function ImpressionArea( as: T = 'div', rootMargin?: string, areaThreshold?: number, - timeThreshold?: number, + timeThresholdMs?: number, onImpressionStart?: () => void, onImpressionEnd?: () => void, ref?: Ref>, @@ -39,7 +39,7 @@ function ImpressionArea( /> @@ -90,7 +90,7 @@ function App() { console.log('Element entered view')} onImpressionEnd={() => console.log('Element exited view')} - timeThreshold={1000} + timeThresholdMs={1000} areaThreshold={0.5} >
Track me!
diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/es/useAvoidKeyboard.md b/packages/react-simplikit/src/hooks/useAvoidKeyboard/es/useAvoidKeyboard.md index 86b14d1c..2985c9bb 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/es/useAvoidKeyboard.md +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/es/useAvoidKeyboard.md @@ -28,7 +28,7 @@ function useAvoidKeyboard( 'Desplazamiento inferior base en píxeles cuando el teclado está oculto. Útil para tener en cuenta el área del indicador de inicio del iPhone.', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/ja/useAvoidKeyboard.md b/packages/react-simplikit/src/hooks/useAvoidKeyboard/ja/useAvoidKeyboard.md index 2da266b3..df0c4e96 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/ja/useAvoidKeyboard.md +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/ja/useAvoidKeyboard.md @@ -28,7 +28,7 @@ function useAvoidKeyboard( 'キーボードが非表示のときの、下端からの基本オフセット(ピクセル単位)です。iPhone のホームインジケーター領域を考慮する際に役立ちます。', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/ko/useAvoidKeyboard.md b/packages/react-simplikit/src/hooks/useAvoidKeyboard/ko/useAvoidKeyboard.md index 70807a75..30e2b5fe 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/ko/useAvoidKeyboard.md +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/ko/useAvoidKeyboard.md @@ -26,7 +26,7 @@ function useAvoidKeyboard( '키보드가 숨겨져 있을 때 기본 하단 오프셋(px)이에요. iPhone의 홈 인디케이터 영역을 고려할 때 유용해요.', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.md b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.md index 9f3cc571..a87b929b 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.md +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.md @@ -28,7 +28,7 @@ function useAvoidKeyboard( 'Base bottom offset in pixels when the keyboard is hidden. Useful for accounting for the iPhone home indicator area.', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.test.ts b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.test.ts index 7be61af9..e4770535 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.test.ts +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.test.ts @@ -56,7 +56,7 @@ describe('useAvoidKeyboard', () => { }); it('should apply custom transition duration', () => { - const { result } = renderHook(() => useAvoidKeyboard({ transitionDuration: 300 })); + const { result } = renderHook(() => useAvoidKeyboard({ transitionDurationMs: 300 })); expect(result.current.style.transition).toBe('transform 300ms ease-out'); }); @@ -75,7 +75,7 @@ describe('useAvoidKeyboard', () => { const { result } = renderHook(() => useAvoidKeyboard({ safeAreaBottom: 30, - transitionDuration: 150, + transitionDurationMs: 150, transitionTimingFunction: 'linear', }) ); diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.ts b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.ts index bb9d8736..7fc5fc07 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.ts +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/useAvoidKeyboard.ts @@ -13,7 +13,7 @@ type UseAvoidKeyboardOptions = { * Transition duration in milliseconds for smooth animation. * @default 200 */ - transitionDuration?: number; + transitionDurationMs?: number; /** * Transition timing function for the animation. * @default 'ease-out' @@ -42,7 +42,7 @@ type UseAvoidKeyboardResult = { * * @param {UseAvoidKeyboardOptions} [options] - Configuration options. * @param {number} [options.safeAreaBottom=0] - Base bottom offset in pixels when the keyboard is hidden. Useful for accounting for the iPhone home indicator area. - * @param {number} [options.transitionDuration=200] - Transition duration in milliseconds for smooth animation. + * @param {number} [options.transitionDurationMs=200] - Transition duration in milliseconds for smooth animation. * @param {CSSProperties['transitionTimingFunction']} [options.transitionTimingFunction='ease-out'] - Transition timing function for the animation. * @param {boolean} [options.immediate=true] - If true, gets the initial keyboard height on mount. * @@ -91,7 +91,7 @@ type UseAvoidKeyboardResult = { export function useAvoidKeyboard(options: UseAvoidKeyboardOptions = {}): UseAvoidKeyboardResult { const { safeAreaBottom = 0, - transitionDuration = 200, + transitionDurationMs = 200, transitionTimingFunction = 'ease-out', immediate = true, } = options; @@ -103,9 +103,9 @@ export function useAvoidKeyboard(options: UseAvoidKeyboardOptions = {}): UseAvoi return { transform: `translateY(${translateY}px)`, - transition: `transform ${transitionDuration}ms ${transitionTimingFunction}`, + transition: `transform ${transitionDurationMs}ms ${transitionTimingFunction}`, }; - }, [keyboardHeight, safeAreaBottom, transitionDuration, transitionTimingFunction]); + }, [keyboardHeight, safeAreaBottom, transitionDurationMs, transitionTimingFunction]); return { style }; } diff --git a/packages/react-simplikit/src/hooks/useAvoidKeyboard/zh-Hans/useAvoidKeyboard.md b/packages/react-simplikit/src/hooks/useAvoidKeyboard/zh-Hans/useAvoidKeyboard.md index 86bb71d1..90b2eca0 100644 --- a/packages/react-simplikit/src/hooks/useAvoidKeyboard/zh-Hans/useAvoidKeyboard.md +++ b/packages/react-simplikit/src/hooks/useAvoidKeyboard/zh-Hans/useAvoidKeyboard.md @@ -26,7 +26,7 @@ function useAvoidKeyboard( '键盘隐藏时的基础底部偏移量(像素)。用于考虑 iPhone 主页指示器区域。', }, { - name: 'options.transitionDuration', + name: 'options.transitionDurationMs', type: 'number', required: false, defaultValue: '200', diff --git a/packages/react-simplikit/src/hooks/useDebounce/es/useDebounce.md b/packages/react-simplikit/src/hooks/useDebounce/es/useDebounce.md index df9491e0..02670516 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/es/useDebounce.md +++ b/packages/react-simplikit/src/hooks/useDebounce/es/useDebounce.md @@ -3,8 +3,8 @@ `useDebounce` es un Hook de React que devuelve una versión con debounce del callback que proporcionas. Te ayuda a optimizar la gestión de eventos al retrasar la ejecución de la función y agrupar varias llamadas en una sola. -Con las opciones predeterminadas, la última llamada se ejecuta después de que transcurran `wait` milisegundos sin otra llamada. -Las llamadas pendientes se cancelan cuando el componente se desmonta o cuando cambia `wait`, `leading` o `trailing`. +Con las opciones predeterminadas, la última llamada se ejecuta después de que transcurran `debounceMs` milisegundos sin otra llamada. +Las llamadas pendientes se cancelan cuando el componente se desmonta o cuando cambia `debounceMs`, `leading` o `trailing`. Cuando llamas a `.cancel()`, solo cancelas un callback pendiente, no una solicitud de red que ya haya comenzado. El ejemplo muestra la consulta de forma local. Para buscar en un servidor, pasa el callback de búsqueda de tu aplicación como primer argumento a `useDebounce`. @@ -14,7 +14,7 @@ de búsqueda de tu aplicación como primer argumento a `useDebounce`. ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -30,7 +30,7 @@ function useDebounce unknown>( diff --git a/packages/react-simplikit/src/hooks/useDebounce/ja/useDebounce.md b/packages/react-simplikit/src/hooks/useDebounce/ja/useDebounce.md index fbdbbc8f..5da1b45a 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/ja/useDebounce.md +++ b/packages/react-simplikit/src/hooks/useDebounce/ja/useDebounce.md @@ -3,8 +3,8 @@ `useDebounce` は、渡されたコールバック関数をデバウンスする React フックです。 関数の実行を遅らせ、複数の呼び出しを一度にまとめることで、イベント処理を最適化します。 -デフォルトのオプションでは、最後の呼び出しから `wait` ミリ秒の間に次の呼び出しがなければ、その最後の呼び出しが実行されます。 -コンポーネントのアンマウント時、または `wait`、`leading`、`trailing` の変更時には、実行待ちの呼び出しをキャンセルします。 +デフォルトのオプションでは、最後の呼び出しから `debounceMs` ミリ秒の間に次の呼び出しがなければ、その最後の呼び出しが実行されます。 +コンポーネントのアンマウント時、または `debounceMs`、`leading`、`trailing` の変更時には、実行待ちの呼び出しをキャンセルします。 `.cancel()` がキャンセルするのは実行待ちのコールバックだけで、すでに開始されたネットワークリクエストはキャンセルしません。 使用例では、クエリをローカルに表示しています。サーバーで検索するには、アプリケーションの 検索用コールバックを `useDebounce` の第 1 引数に渡してください。 @@ -14,7 +14,7 @@ ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -30,7 +30,7 @@ function useDebounce unknown>( diff --git a/packages/react-simplikit/src/hooks/useDebounce/ko/useDebounce.md b/packages/react-simplikit/src/hooks/useDebounce/ko/useDebounce.md index 6f9869c9..ed41ea6c 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/ko/useDebounce.md +++ b/packages/react-simplikit/src/hooks/useDebounce/ko/useDebounce.md @@ -2,14 +2,14 @@ `useDebounce`는 제공된 콜백 함수의 디바운스 버전을 반환하는 리액트 훅이에요. 함수 실행을 지연시키고 여러 호출을 하나로 그룹화하여 이벤트 처리를 최적화하는 데 도움을 줘요. -기본 옵션에서는 추가 호출 없이 `wait` 밀리초가 지나면 마지막 호출을 실행해요. 컴포넌트가 언마운트되거나 `wait`, `leading`, `trailing`이 바뀌면 대기 중인 호출을 취소해요. `.cancel()`은 대기 중인 콜백만 취소하고 이미 시작한 네트워크 요청은 취소하지 않아요. 예제는 검색어를 로컬에 표시해요. 서버에서 검색하려면 `useDebounce`의 첫 번째 인자로 애플리케이션의 검색 콜백을 전달하세요. +기본 옵션에서는 추가 호출 없이 `debounceMs` 밀리초가 지나면 마지막 호출을 실행해요. 컴포넌트가 언마운트되거나 `debounceMs`, `leading`, `trailing`이 바뀌면 대기 중인 호출을 취소해요. `.cancel()`은 대기 중인 콜백만 취소하고 이미 시작한 네트워크 요청은 취소하지 않아요. 예제는 검색어를 로컬에 표시해요. 서버에서 검색하려면 `useDebounce`의 첫 번째 인자로 애플리케이션의 검색 콜백을 전달하세요. ## 인터페이스 ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -25,7 +25,7 @@ function useDebounce unknown>( diff --git a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.md b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.md index 3ece3aab..375b2c0e 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.md +++ b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.md @@ -3,8 +3,8 @@ `useDebounce` is a React hook that returns a debounced version of the provided callback function. It helps optimize event handling by delaying function execution and grouping multiple calls into one. -With the default options, the last call runs after `wait` milliseconds without another call. -Pending calls are cancelled when the component unmounts or when `wait`, `leading`, or `trailing` changes. +With the default options, the last call runs after `debounceMs` milliseconds without another call. +Pending calls are cancelled when the component unmounts or when `debounceMs`, `leading`, or `trailing` changes. Calling `.cancel()` only cancels a pending callback, not an already-started network request. The example displays the query locally. To search a server, pass your application's search callback as the first argument to `useDebounce`. @@ -14,7 +14,7 @@ search callback as the first argument to `useDebounce`. ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -30,7 +30,7 @@ function useDebounce unknown>( diff --git a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts index 7e535687..db29d3e0 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts +++ b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts @@ -16,15 +16,15 @@ type DebounceOptions = { * `useDebounce` is a React hook that returns a debounced version of the provided callback function. * It helps optimize event handling by delaying function execution and grouping multiple calls into one. * - * With the default options, the last call runs after `wait` milliseconds without another call. - * Pending calls are cancelled when the component unmounts or when `wait`, `leading`, or `trailing` changes. + * With the default options, the last call runs after `debounceMs` milliseconds without another call. + * Pending calls are cancelled when the component unmounts or when `debounceMs`, `leading`, or `trailing` changes. * Calling `.cancel()` only cancels a pending callback, not an already-started network request. * The example displays the query locally. To search a server, pass your application's * search callback as the first argument to `useDebounce`. * * @template {(...args: any[]) => unknown} F - The type of the callback function. * @param {F} callback - The function to debounce. - * @param {number} wait - The number of milliseconds to delay the function execution. + * @param {number} debounceMs - The number of milliseconds to delay the function execution. * @param {DebounceOptions} [options] - Configuration options for debounce behavior. * @param {boolean} [options.leading=false] - If `true`, the function is called at the start of the sequence. * @param {boolean} [options.trailing=true] - If `true`, the function is called at the end of the sequence. @@ -63,7 +63,7 @@ type DebounceOptions = { */ export function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options: DebounceOptions = {} ): DebouncedFunction { const preservedCallback = usePreservedCallback(callback) as F; @@ -84,8 +84,8 @@ export function useDebounce unknown>( }, [leading, trailing]); const debounced = useMemo(() => { - return debounce(preservedCallback, wait, { edges }); - }, [preservedCallback, wait, edges]); + return debounce(preservedCallback, debounceMs, { edges }); + }, [preservedCallback, debounceMs, edges]); useEffect( function cancelDebouncedOnUnmount() { diff --git a/packages/react-simplikit/src/hooks/useDebounce/zh-Hans/useDebounce.md b/packages/react-simplikit/src/hooks/useDebounce/zh-Hans/useDebounce.md index 22481e46..b00c6990 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/zh-Hans/useDebounce.md +++ b/packages/react-simplikit/src/hooks/useDebounce/zh-Hans/useDebounce.md @@ -2,14 +2,14 @@ `useDebounce` 是一个返回所提供回调函数防抖版本的 React Hook。它通过延迟函数执行并将多次调用合并为一次,帮助优化事件处理。 -使用默认选项时,最后一次调用会在没有再次调用的 `wait` 毫秒后执行。当组件卸载,或 `wait`、`leading`、`trailing` 发生变化时,待执行的调用会被取消。调用 `.cancel()` 只会取消一个待执行的回调,而不会取消一个已经开始的网络请求。该示例在本地显示查询。要搜索服务器,请将应用程序的搜索回调作为第一个参数传给 `useDebounce`。 +使用默认选项时,最后一次调用会在没有再次调用的 `debounceMs` 毫秒后执行。当组件卸载,或 `debounceMs`、`leading`、`trailing` 发生变化时,待执行的调用会被取消。调用 `.cancel()` 只会取消一个待执行的回调,而不会取消一个已经开始的网络请求。该示例在本地显示查询。要搜索服务器,请将应用程序的搜索回调作为第一个参数传给 `useDebounce`。 ## 接口 ```ts function useDebounce unknown>( callback: F, - wait: number, + debounceMs: number, options?: DebounceOptions ): F & { cancel: () => void }; ``` @@ -25,7 +25,7 @@ function useDebounce unknown>( diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/es/useDebouncedCallback.md b/packages/react-simplikit/src/hooks/useDebouncedCallback/es/useDebouncedCallback.md index e0299431..036d889c 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/es/useDebouncedCallback.md +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/es/useDebouncedCallback.md @@ -27,7 +27,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; 'El callback al que quieres aplicar debounce. Las llamadas con el mismo valor que el último enviado se omiten.', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -70,7 +70,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/ja/useDebouncedCallback.md b/packages/react-simplikit/src/hooks/useDebouncedCallback/ja/useDebouncedCallback.md index cbe95f47..ce7b1948 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/ja/useDebouncedCallback.md +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/ja/useDebouncedCallback.md @@ -27,7 +27,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; 'デバウンスするコールバックです。最後に渡した値と同じ値での呼び出しはスキップします。', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -70,7 +70,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/ko/useDebouncedCallback.md b/packages/react-simplikit/src/hooks/useDebouncedCallback/ko/useDebouncedCallback.md index 16149f59..9f05423b 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/ko/useDebouncedCallback.md +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/ko/useDebouncedCallback.md @@ -24,7 +24,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; '디바운스할 콜백이에요. 마지막으로 전달된 값과 같은 값으로 호출하면 건너뛰어요.', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -67,7 +67,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.md b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.md index 7c70c45e..2dcc8862 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.md +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.md @@ -27,7 +27,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; 'The callback to debounce. A call with the same value as the last forwarded one is skipped.', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -70,7 +70,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.spec.ts b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.spec.ts index e6c39e91..3b9113ba 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.spec.ts +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.spec.ts @@ -11,14 +11,14 @@ describe('useDebouncedCallback', () => { it('is safe on server side rendering', () => { const onChange = vi.fn(); - renderHookSSR.serverOnly(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + renderHookSSR.serverOnly(() => useDebouncedCallback({ onChange, debounceMs: 100 })); expect(onChange).not.toHaveBeenCalled(); }); it('should debounce the callback with the specified time threshold', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current(true); expect(onChange).not.toBeCalled(); @@ -44,7 +44,7 @@ describe('useDebouncedCallback', () => { it('should handle leading edge', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100, leading: true })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100, leading: true })); result.current(true); expect(onChange).toBeCalledTimes(1); @@ -64,7 +64,7 @@ describe('useDebouncedCallback', () => { it('should not trigger callback if value has not changed', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current(true); vi.advanceTimersByTime(100); @@ -77,7 +77,7 @@ describe('useDebouncedCallback', () => { it('forwards a string value to onChange', () => { const onChange = vi.fn<(value: string) => void>(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current('react'); vi.advanceTimersByTime(100); @@ -87,7 +87,7 @@ describe('useDebouncedCallback', () => { it('invokes the callback when the first value is false', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current(false); vi.advanceTimersByTime(100); @@ -98,7 +98,7 @@ describe('useDebouncedCallback', () => { it('discards a pending value when the caller returns to the last forwarded one', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current('seoul'); vi.advanceTimersByTime(100); @@ -114,7 +114,7 @@ describe('useDebouncedCallback', () => { it('should cleanup on unmount', async () => { const onChange = vi.fn(); - const { result, unmount } = await renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100 })); + const { result, unmount } = await renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100 })); result.current(true); unmount(); @@ -125,7 +125,7 @@ describe('useDebouncedCallback', () => { it('debounces a stream of distinct values when leading is true', () => { const onChange = vi.fn(); - const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, timeThreshold: 100, leading: true })); + const { result } = renderHookSSR(() => useDebouncedCallback({ onChange, debounceMs: 100, leading: true })); for (const value of ['a', 'b', 'c']) { result.current(value); @@ -140,7 +140,7 @@ describe('useDebouncedCallback', () => { it('forwards only the first value of a burst when trailing is false', () => { const onChange = vi.fn(); const { result } = renderHookSSR(() => - useDebouncedCallback({ onChange, timeThreshold: 100, leading: true, trailing: false }) + useDebouncedCallback({ onChange, debounceMs: 100, leading: true, trailing: false }) ); for (const value of ['a', 'b', 'c']) { diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.ts b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.ts index d1250e18..6122591f 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.ts +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/useDebouncedCallback.ts @@ -26,7 +26,7 @@ const NOT_INVOKED = Symbol('NOT_INVOKED'); * @template T - The type of the value passed to `onChange`. * @param {Object} options - The options object. * @param {(newValue: T) => void} options.onChange - The callback to debounce. A call with the same value as the last forwarded one is skipped. - * @param {number} options.timeThreshold - The number of milliseconds to delay the function execution. + * @param {number} options.debounceMs - The number of milliseconds to delay the function execution. * @param {boolean} [options.leading=false] - If `true`, the function is called at the start of the sequence. * @param {boolean} [options.trailing=true] - If `true`, the function is called at the end of the sequence. * @@ -38,7 +38,7 @@ const NOT_INVOKED = Symbol('NOT_INVOKED'); * * function SearchInput() { * const [query, setQuery] = useState(''); - * const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, timeThreshold: 300 }); + * const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, debounceMs: 300 }); * * return ( * <> @@ -50,12 +50,12 @@ const NOT_INVOKED = Symbol('NOT_INVOKED'); */ export function useDebouncedCallback({ onChange, - timeThreshold, + debounceMs, leading = false, trailing = true, }: DebounceOptions & { onChange: (newValue: T) => void; - timeThreshold: number; + debounceMs: number; }): (nextValue: T) => void { const lastForwardedRef = useRef(NOT_INVOKED); @@ -69,7 +69,7 @@ export function useDebouncedCallback({ lastForwardedRef.current = nextValue; }, - timeThreshold, + debounceMs, { leading, trailing } ); } diff --git a/packages/react-simplikit/src/hooks/useDebouncedCallback/zh-Hans/useDebouncedCallback.md b/packages/react-simplikit/src/hooks/useDebouncedCallback/zh-Hans/useDebouncedCallback.md index 0bf0c5fe..1974015e 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedCallback/zh-Hans/useDebouncedCallback.md +++ b/packages/react-simplikit/src/hooks/useDebouncedCallback/zh-Hans/useDebouncedCallback.md @@ -26,7 +26,7 @@ function useDebouncedCallback(options: Object): (nextValue: T) => void; '要被防抖的回调。携带与上一次转发的值相同的调用会被跳过。', }, { - name: 'options.timeThreshold', + name: 'options.debounceMs', type: 'number', required: true, description: @@ -69,7 +69,7 @@ function SearchInput() { const [query, setQuery] = useState(''); const setQueryDebounced = useDebouncedCallback({ onChange: setQuery, - timeThreshold: 300, + debounceMs: 300, }); return ( diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/es/useDebouncedValue.md b/packages/react-simplikit/src/hooks/useDebouncedValue/es/useDebouncedValue.md index 4b49d1e5..90231636 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/es/useDebouncedValue.md +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/es/useDebouncedValue.md @@ -2,7 +2,7 @@ `useDebouncedValue` es un Hook de React que devuelve una copia con debounce del valor que proporcionas. Mantienes el control del estado; el Hook solo retrasa la actualización del valor devuelto para que refleje ese estado. -El valor devuelto se actualiza `wait` milisegundos después del último cambio, lo que te resulta útil +El valor devuelto se actualiza `debounceMs` milisegundos después del último cambio, lo que te resulta útil para obtener una consulta de búsqueda o un valor para validar a partir de un estado que cambia rápidamente. Las actualizaciones pendientes se cancelan cuando el componente se desmonta. El ejemplo muestra la @@ -13,7 +13,7 @@ al montar el componente, por lo que, con `leading: true`, el primer cambio poste Si tanto `leading` como `trailing` son `false`, el valor devuelto nunca se actualiza. El valor se compara por referencia. Si pasas un objeto o arreglo nuevo en cada renderizado, -el valor devuelto seguirá actualizándose cada `wait` milisegundos; estabiliza primero la referencia, +el valor devuelto seguirá actualizándose cada `debounceMs` milisegundos; estabiliza primero la referencia, por ejemplo, con `usePreservedReference`. ## Interfaz @@ -21,7 +21,7 @@ por ejemplo, con `usePreservedReference`. ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -37,7 +37,7 @@ function useDebouncedValue( @@ -61,7 +61,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - 'Si es true, el último cambio se aplica después de wait milisegundos.', + 'Si es true, el último cambio se aplica después de debounceMs milisegundos.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/ja/useDebouncedValue.md b/packages/react-simplikit/src/hooks/useDebouncedValue/ja/useDebouncedValue.md index 5dea7696..b8d90cf8 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/ja/useDebouncedValue.md +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/ja/useDebouncedValue.md @@ -2,7 +2,7 @@ `useDebouncedValue` は、渡された値をデバウンスした値として返す React フックです。 状態は引き続き呼び出し側で管理し、このフックは戻り値がその状態に追従するタイミングだけを遅らせます。 -戻り値は最後の変更から `wait` ミリ秒後に更新されるため、頻繁に変わる状態から +戻り値は最後の変更から `debounceMs` ミリ秒後に更新されるため、頻繁に変わる状態から 検索クエリやバリデーション用の入力値を得る場合に便利です。 コンポーネントのアンマウント時に、保留中の更新をキャンセルします。使用例では、 @@ -13,7 +13,7 @@ `leading` と `trailing` が両方とも `false` の場合、戻り値は更新されません。 値は参照で比較します。レンダリングのたびに新しいオブジェクトや配列を渡すと、 -戻り値が `wait` ミリ秒ごとに更新され続けます。まず `usePreservedReference` などを使って、 +戻り値が `debounceMs` ミリ秒ごとに更新され続けます。まず `usePreservedReference` などを使って、 参照を安定させてください。 ## インターフェース @@ -21,7 +21,7 @@ ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -37,7 +37,7 @@ function useDebouncedValue( @@ -61,7 +61,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - 'true の場合、wait ミリ秒後に最後の変更を反映します。', + 'true の場合、debounceMs ミリ秒後に最後の変更を反映します。', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/ko/useDebouncedValue.md b/packages/react-simplikit/src/hooks/useDebouncedValue/ko/useDebouncedValue.md index 1d71282d..5d4d7fc0 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/ko/useDebouncedValue.md +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/ko/useDebouncedValue.md @@ -1,19 +1,19 @@ # useDebouncedValue -`useDebouncedValue`는 전달받은 값의 디바운스된 사본을 반환하는 리액트 훅이에요. 상태는 호출자가 그대로 소유하고, 훅은 반환값이 그 상태를 얼마나 늦게 따라갈지만 조절해요. 반환값은 마지막 변경 뒤 `wait` 밀리초가 지나면 갱신되므로, 빠르게 바뀌는 상태에서 검색어나 검증 대상을 파생할 때 유용해요. +`useDebouncedValue`는 전달받은 값의 디바운스된 사본을 반환하는 리액트 훅이에요. 상태는 호출자가 그대로 소유하고, 훅은 반환값이 그 상태를 얼마나 늦게 따라갈지만 조절해요. 반환값은 마지막 변경 뒤 `debounceMs` 밀리초가 지나면 갱신되므로, 빠르게 바뀌는 상태에서 검색어나 검증 대상을 파생할 때 유용해요. 컴포넌트가 언마운트되면 대기 중인 갱신을 취소해요. 예제는 별도의 검색 서비스나 추가 컴포넌트 없이 지연된 검색어를 표시해요. 첫 렌더와 서버 렌더에서는 값을 그대로 돌려줘요. 마운트 시점에는 갱신을 예약하지 않으므로 `leading: true`이면 마운트 뒤 첫 변경이 즉시 반영돼요. `leading`과 `trailing`이 모두 `false`이면 반환값은 갱신되지 않아요. -값은 참조로 비교해요. 렌더마다 새 객체나 배열을 전달하면 반환값이 `wait` 밀리초마다 계속 갱신되니, `usePreservedReference` 같은 방법으로 참조를 먼저 안정화하세요. +값은 참조로 비교해요. 렌더마다 새 객체나 배열을 전달하면 반환값이 `debounceMs` 밀리초마다 계속 갱신되니, `usePreservedReference` 같은 방법으로 참조를 먼저 안정화하세요. ## 인터페이스 ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -29,7 +29,7 @@ function useDebouncedValue( @@ -53,7 +53,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - 'true이면 마지막 변경을 wait 밀리초 뒤에 반영해요.', + 'true이면 마지막 변경을 debounceMs 밀리초 뒤에 반영해요.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.md b/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.md index 1d7cbc5c..0cd15500 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.md +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.md @@ -2,7 +2,7 @@ `useDebouncedValue` is a React hook that returns a debounced copy of the given value. The caller keeps owning the state; the hook only delays how quickly the returned value follows it. -The returned value updates `wait` milliseconds after the last change, which is useful for +The returned value updates `debounceMs` milliseconds after the last change, which is useful for deriving a search query or a validation input from fast-changing state. Pending updates are cancelled when the component unmounts. The example displays the @@ -13,7 +13,7 @@ on mount, so with `leading: true` the first change after mount is applied immedi If both `leading` and `trailing` are `false`, the returned value never updates. The value is compared by reference. Passing a new object or array on every render keeps -the returned value updating every `wait` milliseconds; stabilize the reference first, for +the returned value updating every `debounceMs` milliseconds; stabilize the reference first, for example with `usePreservedReference`. ## Interface @@ -21,7 +21,7 @@ example with `usePreservedReference`. ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -37,7 +37,7 @@ function useDebouncedValue( @@ -61,7 +61,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - 'If true, the last change is applied after wait milliseconds.', + 'If true, the last change is applied after debounceMs milliseconds.', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.ts b/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.ts index 52a69a49..bc40e0fc 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.ts +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/useDebouncedValue.ts @@ -11,7 +11,7 @@ type DebounceOptions = { * @description * `useDebouncedValue` is a React hook that returns a debounced copy of the given value. * The caller keeps owning the state; the hook only delays how quickly the returned value follows it. - * The returned value updates `wait` milliseconds after the last change, which is useful for + * The returned value updates `debounceMs` milliseconds after the last change, which is useful for * deriving a search query or a validation input from fast-changing state. * * Pending updates are cancelled when the component unmounts. The example displays the @@ -22,15 +22,15 @@ type DebounceOptions = { * If both `leading` and `trailing` are `false`, the returned value never updates. * * The value is compared by reference. Passing a new object or array on every render keeps - * the returned value updating every `wait` milliseconds; stabilize the reference first, for + * the returned value updating every `debounceMs` milliseconds; stabilize the reference first, for * example with `usePreservedReference`. * * @template T - The type of the value. * @param {T} value - The value to debounce. - * @param {number} wait - The number of milliseconds to wait after the last change before updating. + * @param {number} debounceMs - The number of milliseconds to wait after the last change before updating. * @param {DebounceOptions} [options] - Configuration options for debounce behavior. * @param {boolean} [options.leading=false] - If `true`, the first change after an idle period is applied immediately. - * @param {boolean} [options.trailing=true] - If `true`, the last change is applied after `wait` milliseconds. + * @param {boolean} [options.trailing=true] - If `true`, the last change is applied after `debounceMs` milliseconds. * * @returns {T} The debounced value. * @@ -55,11 +55,11 @@ type DebounceOptions = { */ export function useDebouncedValue( value: T, - wait: number, + debounceMs: number, { leading = false, trailing = true }: DebounceOptions = {} ): T { const [debouncedValue, setDebouncedValue] = useState(() => value); - const debounced = useDebounce((next: T) => setDebouncedValue(() => next), wait, { leading, trailing }); + const debounced = useDebounce((next: T) => setDebouncedValue(() => next), debounceMs, { leading, trailing }); const lastForwardedRef = useRef(value); useEffect( diff --git a/packages/react-simplikit/src/hooks/useDebouncedValue/zh-Hans/useDebouncedValue.md b/packages/react-simplikit/src/hooks/useDebouncedValue/zh-Hans/useDebouncedValue.md index 9d785f4b..d320c989 100644 --- a/packages/react-simplikit/src/hooks/useDebouncedValue/zh-Hans/useDebouncedValue.md +++ b/packages/react-simplikit/src/hooks/useDebouncedValue/zh-Hans/useDebouncedValue.md @@ -1,19 +1,19 @@ # useDebouncedValue -`useDebouncedValue` 是一个返回所给值防抖副本的 React Hook。调用方始终拥有该状态;Hook 只延迟返回值的跟随速度。返回值会在最后一次变化后 `wait` 毫秒更新,这适用于从快速变化的状态中推导搜索查询或校验输入。 +`useDebouncedValue` 是一个返回所给值防抖副本的 React Hook。调用方始终拥有该状态;Hook 只延迟返回值的跟随速度。返回值会在最后一次变化后 `debounceMs` 毫秒更新,这适用于从快速变化的状态中推导搜索查询或校验输入。 当组件卸载时,待执行的更新会被取消。该示例展示了延迟后的查询,无需搜索服务或额外的组件。 在首次渲染时和服务端,该值会原样返回。挂载时永远不会安排变化,因此使用 `leading: true` 时,挂载后的第一次变化会立即应用。如果 `leading` 和 `trailing` 都为 `false`,返回值永远不会更新。 -该值通过引用进行比较。如果每次渲染都传入新的对象或数组,返回值会每 `wait` 毫秒更新一次;请先稳定引用,例如使用 `usePreservedReference`。 +该值通过引用进行比较。如果每次渲染都传入新的对象或数组,返回值会每 `debounceMs` 毫秒更新一次;请先稳定引用,例如使用 `usePreservedReference`。 ## 接口 ```ts function useDebouncedValue( value: T, - wait: number, + debounceMs: number, options?: DebounceOptions ): T; ``` @@ -29,7 +29,7 @@ function useDebouncedValue( @@ -53,7 +53,7 @@ function useDebouncedValue( required: false, defaultValue: 'true', description: - '如果为 true,最后一次变化会在 wait 毫秒后应用。', + '如果为 true,最后一次变化会在 debounceMs 毫秒后应用。', }, ]" /> diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/es/useImpressionRef.md b/packages/react-simplikit/src/hooks/useImpressionRef/es/useImpressionRef.md index c54214d9..c3a2cb77 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/es/useImpressionRef.md +++ b/packages/react-simplikit/src/hooks/useImpressionRef/es/useImpressionRef.md @@ -33,7 +33,7 @@ function useImpressionRef( description: 'Callback que se ejecuta cuando el elemento sale del área visible', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -73,7 +73,7 @@ function Component() { onImpressionStart: () => console.log('El elemento entró en el área visible'), onImpressionEnd: () => console.log('El elemento salió del área visible'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/ja/useImpressionRef.md b/packages/react-simplikit/src/hooks/useImpressionRef/ja/useImpressionRef.md index 47ac4e6d..021a7fcb 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/ja/useImpressionRef.md +++ b/packages/react-simplikit/src/hooks/useImpressionRef/ja/useImpressionRef.md @@ -33,7 +33,7 @@ function useImpressionRef( description: '要素が表示領域から出たときに実行するコールバック関数。', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -72,7 +72,7 @@ function Component() { const ref = useImpressionRef({ onImpressionStart: () => console.log('Element entered view'), onImpressionEnd: () => console.log('Element exited view'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/ko/useImpressionRef.md b/packages/react-simplikit/src/hooks/useImpressionRef/ko/useImpressionRef.md index 8cd2ba0e..f20c0216 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/ko/useImpressionRef.md +++ b/packages/react-simplikit/src/hooks/useImpressionRef/ko/useImpressionRef.md @@ -32,7 +32,7 @@ function useImpressionRef( description: '요소가 뷰에서 나갈 때 실행되는 콜백 함수예요', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -71,7 +71,7 @@ function Component() { const ref = useImpressionRef({ onImpressionStart: () => console.log('Element entered view'), onImpressionEnd: () => console.log('Element exited view'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.md b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.md index 14602f6a..63b7f3c2 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.md +++ b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.md @@ -33,7 +33,7 @@ function useImpressionRef( description: 'Callback function executed when the element exits the view', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -72,7 +72,7 @@ function Component() { const ref = useImpressionRef({ onImpressionStart: () => console.log('Element entered view'), onImpressionEnd: () => console.log('Element exited view'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.spec.ts b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.spec.ts index de3d2c93..d0ccd32e 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.spec.ts +++ b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.spec.ts @@ -30,7 +30,7 @@ describe('useImpressionRef', () => { const defaultOptions: UseImpressionRefOptions = { onImpressionStart: mockOnImpressionStart, onImpressionEnd: mockOnImpressionEnd, - timeThreshold: 100, + timeThresholdMs: 100, rootMargin: '0px', areaThreshold: 0.5, }; diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts index b6f88a74..b8d07740 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts +++ b/packages/react-simplikit/src/hooks/useImpressionRef/useImpressionRef.ts @@ -10,7 +10,7 @@ export type UseImpressionRefOptions = Partial<{ onImpressionEnd: () => void; rootMargin: string; areaThreshold: number; - timeThreshold: number; + timeThresholdMs: number; }>; /** @@ -22,7 +22,7 @@ export type UseImpressionRefOptions = Partial<{ * @param {UseImpressionRefOptions} options - Options for tracking the element's visibility. * @param {() => void} [options.onImpressionStart] - Callback function executed when the element enters the view * @param {() => void} [options.onImpressionEnd] - Callback function executed when the element exits the view - * @param {number} [options.timeThreshold=0] - Minimum time the element must be visible (in milliseconds) + * @param {number} [options.timeThresholdMs=0] - Minimum time the element must be visible (in milliseconds) * @param {number} [options.areaThreshold=0] - Minimum ratio of the element that must be visible (0 to 1) * @param {string} options.rootMargin - Margin to adjust the detection area * @@ -35,7 +35,7 @@ export type UseImpressionRefOptions = Partial<{ * const ref = useImpressionRef({ * onImpressionStart: () => console.log('Element entered view'), * onImpressionEnd: () => console.log('Element exited view'), - * timeThreshold: 1000, + * timeThresholdMs: 1000, * areaThreshold: 0.5, * }); * @@ -47,7 +47,7 @@ export function useImpressionRef({ onImpressionEnd = () => {}, rootMargin, areaThreshold = 0, - timeThreshold = 0, + timeThresholdMs = 0, }: UseImpressionRefOptions) { const impressionStartHandler = usePreservedCallback(onImpressionStart); const impressionEndHandler = usePreservedCallback(onImpressionEnd); @@ -60,7 +60,7 @@ export function useImpressionRef({ // or the tab is hidden before it ever intersects); an end without a start must not be emitted. const hasImpressionStartedRef = useRef(false); const impressionEventHandler = useDebouncedCallback({ - timeThreshold, + debounceMs: timeThresholdMs, onChange: (impressed: boolean) => { if (impressed) { hasImpressionStartedRef.current = true; diff --git a/packages/react-simplikit/src/hooks/useImpressionRef/zh-Hans/useImpressionRef.md b/packages/react-simplikit/src/hooks/useImpressionRef/zh-Hans/useImpressionRef.md index 79799997..485cf040 100644 --- a/packages/react-simplikit/src/hooks/useImpressionRef/zh-Hans/useImpressionRef.md +++ b/packages/react-simplikit/src/hooks/useImpressionRef/zh-Hans/useImpressionRef.md @@ -32,7 +32,7 @@ function useImpressionRef( description: '元素退出视图时执行的回调函数', }, { - name: 'options.timeThreshold', + name: 'options.timeThresholdMs', type: 'number', required: false, defaultValue: '0', @@ -71,7 +71,7 @@ function Component() { const ref = useImpressionRef({ onImpressionStart: () => console.log('Element entered view'), onImpressionEnd: () => console.log('Element exited view'), - timeThreshold: 1000, + timeThresholdMs: 1000, areaThreshold: 0.5, }); diff --git a/packages/react-simplikit/src/hooks/useLongPress/es/useLongPress.md b/packages/react-simplikit/src/hooks/useLongPress/es/useLongPress.md index 28fc1c54..18b2bfba 100644 --- a/packages/react-simplikit/src/hooks/useLongPress/es/useLongPress.md +++ b/packages/react-simplikit/src/hooks/useLongPress/es/useLongPress.md @@ -27,7 +27,7 @@ function useLongPress( description="Opciones de configuración del comportamiento de la pulsación prolongada." :nested="[ { - name: 'options.delay', + name: 'options.delayMs', type: 'number', required: false, defaultValue: '500', @@ -131,7 +131,7 @@ function ContextMenu() { const [menuVisible, setMenuVisible] = useState(false); const longPressHandlers = useLongPress(() => setMenuVisible(true), { - delay: 400, + delayMs: 400, onClick: () => console.log('Clic normal'), onLongPressEnd: () => console.log('Pulsación prolongada completada'), }); diff --git a/packages/react-simplikit/src/hooks/useLongPress/ja/useLongPress.md b/packages/react-simplikit/src/hooks/useLongPress/ja/useLongPress.md index aa8eb966..69c6f22f 100644 --- a/packages/react-simplikit/src/hooks/useLongPress/ja/useLongPress.md +++ b/packages/react-simplikit/src/hooks/useLongPress/ja/useLongPress.md @@ -27,7 +27,7 @@ function useLongPress( description="長押しの動作を設定するオプションです。" :nested="[ { - name: 'options.delay', + name: 'options.delayMs', type: 'number', required: false, defaultValue: '500', @@ -57,7 +57,7 @@ function useLongPress( type: '(event) => void', required: false, description: - '通常のクリック(押してから delay が経過する前に離す操作)で実行する関数です。省略できます。', + '通常のクリック(押してから delayMs が経過する前に離す操作)で実行する関数です。省略できます。', }, { name: 'options.onLongPressEnd', @@ -131,7 +131,7 @@ function ContextMenu() { const [menuVisible, setMenuVisible] = useState(false); const longPressHandlers = useLongPress(() => setMenuVisible(true), { - delay: 400, + delayMs: 400, onClick: () => console.log('Normal click'), onLongPressEnd: () => console.log('Long press completed'), }); diff --git a/packages/react-simplikit/src/hooks/useLongPress/ko/useLongPress.md b/packages/react-simplikit/src/hooks/useLongPress/ko/useLongPress.md index 14e5392b..02a97ffb 100644 --- a/packages/react-simplikit/src/hooks/useLongPress/ko/useLongPress.md +++ b/packages/react-simplikit/src/hooks/useLongPress/ko/useLongPress.md @@ -26,7 +26,7 @@ function useLongPress( description="길게 누름 동작을 위한 설정 옵션이에요." :nested="[ { - name: 'options.delay', + name: 'options.delayMs', type: 'number', required: false, defaultValue: '500', @@ -130,7 +130,7 @@ function ContextMenu() { const [menuVisible, setMenuVisible] = useState(false); const longPressHandlers = useLongPress(() => setMenuVisible(true), { - delay: 400, + delayMs: 400, onClick: () => console.log('일반 클릭'), onLongPressEnd: () => console.log('길게 누름 완료'), }); diff --git a/packages/react-simplikit/src/hooks/useLongPress/useLongPress.md b/packages/react-simplikit/src/hooks/useLongPress/useLongPress.md index 4990a515..b4bf1ae7 100644 --- a/packages/react-simplikit/src/hooks/useLongPress/useLongPress.md +++ b/packages/react-simplikit/src/hooks/useLongPress/useLongPress.md @@ -27,7 +27,7 @@ function useLongPress( description="Configuration options for the long press behavior." :nested="[ { - name: 'options.delay', + name: 'options.delayMs', type: 'number', required: false, defaultValue: '500', @@ -131,7 +131,7 @@ function ContextMenu() { const [menuVisible, setMenuVisible] = useState(false); const longPressHandlers = useLongPress(() => setMenuVisible(true), { - delay: 400, + delayMs: 400, onClick: () => console.log('Normal click'), onLongPressEnd: () => console.log('Long press completed'), }); diff --git a/packages/react-simplikit/src/hooks/useLongPress/useLongPress.spec.tsx b/packages/react-simplikit/src/hooks/useLongPress/useLongPress.spec.tsx index 70968e52..01ed8936 100644 --- a/packages/react-simplikit/src/hooks/useLongPress/useLongPress.spec.tsx +++ b/packages/react-simplikit/src/hooks/useLongPress/useLongPress.spec.tsx @@ -12,7 +12,7 @@ type PressTargetProps = { }; function PressTarget({ onLongPress, onClick, onLongPressEnd }: PressTargetProps) { - const handlers = useLongPress(onLongPress, { delay: 500, onClick, onLongPressEnd }); + const handlers = useLongPress(onLongPress, { delayMs: 500, onClick, onLongPressEnd }); return ; } @@ -97,7 +97,7 @@ describe('useLongPress', () => { it('should respect custom delay timing', async () => { const onLongPress = vi.fn(); const TestComponent = () => { - const longPressHandlers = useLongPress(onLongPress, { delay: 1000 }); + const longPressHandlers = useLongPress(onLongPress, { delayMs: 1000 }); return (