From 17f6d47324caa7bbf31ae3a9c366e4b42cd062c6 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:52:02 +0900 Subject: [PATCH] refactor: standardize state hook return values --- .changeset/flat-state-hook-returns.md | 5 + .../references/useBooleanState.md | 28 ++--- .../react-simplikit/references/useList.md | 29 ++--- .../react-simplikit/references/useMap.md | 6 +- .../react-simplikit/references/useSet.md | 16 +-- .../references/useStorageState.md | 29 +++-- .../useBooleanState/es/useBooleanState.md | 28 ++--- .../useBooleanState/ja/useBooleanState.md | 28 ++--- .../useBooleanState/ko/useBooleanState.md | 28 ++--- .../hooks/useBooleanState/useBooleanState.md | 28 ++--- .../useBooleanState/useBooleanState.spec.ts | 22 ++-- .../hooks/useBooleanState/useBooleanState.ts | 17 +-- .../zh-Hans/useBooleanState.md | 28 ++--- .../src/hooks/useList/es/useList.md | 29 ++--- .../src/hooks/useList/ja/useList.md | 29 ++--- .../src/hooks/useList/ko/useList.md | 29 ++--- .../src/hooks/useList/useList.md | 29 ++--- .../src/hooks/useList/useList.spec.ts | 62 +++++------ .../src/hooks/useList/useList.ts | 39 +++---- .../src/hooks/useList/zh-Hans/useList.md | 29 ++--- .../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.spec.ts | 70 ++++++------ .../src/hooks/useMap/useMap.ts | 16 +-- .../src/hooks/useMap/zh-Hans/useMap.md | 6 +- .../src/hooks/useSet/es/useSet.md | 16 +-- .../src/hooks/useSet/ja/useSet.md | 16 +-- .../src/hooks/useSet/ko/useSet.md | 16 +-- .../src/hooks/useSet/useSet.md | 16 +-- .../src/hooks/useSet/useSet.spec.ts | 104 +++++++++--------- .../src/hooks/useSet/useSet.ts | 27 ++--- .../src/hooks/useSet/zh-Hans/useSet.md | 16 +-- .../useStorageState/es/useStorageState.md | 23 ++-- .../useStorageState/ja/useStorageState.md | 23 ++-- .../useStorageState/ko/useStorageState.md | 23 ++-- .../hooks/useStorageState/useStorageState.md | 29 +++-- .../useStorageState/useStorageState.spec.ts | 62 +++++------ .../hooks/useStorageState/useStorageState.ts | 54 +++++---- .../zh-Hans/useStorageState.md | 23 ++-- 41 files changed, 548 insertions(+), 554 deletions(-) create mode 100644 .changeset/flat-state-hook-returns.md diff --git a/.changeset/flat-state-hook-returns.md b/.changeset/flat-state-hook-returns.md new file mode 100644 index 00000000..ce8e2788 --- /dev/null +++ b/.changeset/flat-state-hook-returns.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': major +--- + +Return a single object from `useBooleanState`, `useStorageState`, `useList`, `useMap`, and `useSet`. State values and actions are now named fields. diff --git a/packages/plugin/skills/react-simplikit/references/useBooleanState.md b/packages/plugin/skills/react-simplikit/references/useBooleanState.md index 3ef27c3e..845af31a 100644 --- a/packages/plugin/skills/react-simplikit/references/useBooleanState.md +++ b/packages/plugin/skills/react-simplikit/references/useBooleanState.md @@ -6,14 +6,12 @@ It provides functions to set the state to `true`, set it to `false`, and toggle ## Interface ```ts -function useBooleanState( - initialValue: boolean | (() => boolean) = false -): readonly [ - state: boolean, - setTrue: () => void, - setFalse: () => void, - toggle: () => void, -]; +function useBooleanState(initialValue: boolean | (() => boolean) = false): { + value: boolean; + setTrue: () => void; + setFalse: () => void; + toggle: () => void; +}; ``` ### Parameters @@ -28,11 +26,11 @@ function useBooleanState( ## Example ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -37,5 +37,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // Updating the Map -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/plugin/skills/react-simplikit/references/useSet.md b/packages/plugin/skills/react-simplikit/references/useSet.md index 3701fbba..58b45e29 100644 --- a/packages/plugin/skills/react-simplikit/references/useSet.md +++ b/packages/plugin/skills/react-simplikit/references/useSet.md @@ -22,40 +22,40 @@ function useSet(initialState: SetOrValues = new Set()): UseSetReturn; @@ -94,9 +90,12 @@ function useStorageState( import { useStorageState } from 'react-simplikit'; function Counter() { - const [count, setCount] = useStorageState('counter', { - defaultValue: 0, - }); + const { value: count, setValue: setCount } = useStorageState( + 'counter', + { + defaultValue: 0, + } + ); return ( diff --git a/packages/react-simplikit/src/hooks/useBooleanState/es/useBooleanState.md b/packages/react-simplikit/src/hooks/useBooleanState/es/useBooleanState.md index 4cd9664c..93989b7e 100644 --- a/packages/react-simplikit/src/hooks/useBooleanState/es/useBooleanState.md +++ b/packages/react-simplikit/src/hooks/useBooleanState/es/useBooleanState.md @@ -6,14 +6,12 @@ Proporciona funciones para establecer el estado en `true`, establecerlo en `fals ## Interfaz ```ts -function useBooleanState( - initialValue: boolean | (() => boolean) = false -): readonly [ - state: boolean, - setTrue: () => void, - setFalse: () => void, - toggle: () => void, -]; +function useBooleanState(initialValue: boolean | (() => boolean) = false): { + value: boolean; + setTrue: () => void; + setFalse: () => void; + toggle: () => void; +}; ``` ### Parámetros @@ -28,11 +26,11 @@ function useBooleanState( ## Ejemplo ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -37,5 +37,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // Actualizar el Map -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/react-simplikit/src/hooks/useMap/ja/useMap.md b/packages/react-simplikit/src/hooks/useMap/ja/useMap.md index 15f0fe65..1856d161 100644 --- a/packages/react-simplikit/src/hooks/useMap/ja/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/ja/useMap.md @@ -23,13 +23,13 @@ function useMap(initialState: MapOrEntries): UseMapReturn; ## 使用例 ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -37,5 +37,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // Map を更新します -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/react-simplikit/src/hooks/useMap/ko/useMap.md b/packages/react-simplikit/src/hooks/useMap/ko/useMap.md index 0c0ab92e..2d9f138b 100644 --- a/packages/react-simplikit/src/hooks/useMap/ko/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/ko/useMap.md @@ -22,13 +22,13 @@ function useMap(initialState: MapOrEntries): UseMapReturn; ## 예시 ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -36,5 +36,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // 맵 업데이트하기 -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/react-simplikit/src/hooks/useMap/useMap.md b/packages/react-simplikit/src/hooks/useMap/useMap.md index ab50cf3d..f2625d07 100644 --- a/packages/react-simplikit/src/hooks/useMap/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/useMap.md @@ -23,13 +23,13 @@ function useMap(initialState: MapOrEntries): UseMapReturn; ## Example ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -37,5 +37,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // Updating the Map -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/react-simplikit/src/hooks/useMap/useMap.spec.ts b/packages/react-simplikit/src/hooks/useMap/useMap.spec.ts index eaf9dd72..d948758b 100644 --- a/packages/react-simplikit/src/hooks/useMap/useMap.spec.ts +++ b/packages/react-simplikit/src/hooks/useMap/useMap.spec.ts @@ -10,46 +10,46 @@ describe('useMap', () => { const initialMap = new Map([[1, 'initial']]); const { result } = await renderHookSSR(() => useMap(initialMap)); - expect(result.current[0].get(1)).toBe('initial'); + expect(result.current.map.get(1)).toBe('initial'); }); it('should initialize with an array of entries', async () => { const { result } = await renderHookSSR(() => useMap([[1, 'initial']])); - expect(result.current[0].get(1)).toBe('initial'); + expect(result.current.map.get(1)).toBe('initial'); }); it('should initialize with an empty Map when no arguments provided', async () => { const { result } = await renderHookSSR(() => useMap()); - expect(result.current[0].size).toBe(0); + expect(result.current.map.size).toBe(0); }); it('should add a new value to the Map', async () => { const { result, rerender } = await renderHookSSR(() => useMap()); - const [, actions] = result.current; + const actions = result.current; - expect(result.current[0].get(1)).toBeUndefined(); + expect(result.current.map.get(1)).toBeUndefined(); await act(async () => { actions.set(1, 'added'); rerender(); }); - expect(result.current[0].get(1)).toBe('added'); + expect(result.current.map.get(1)).toBe('added'); }); it('should update an existing value in the Map', async () => { const initialMap = new Map([[1, 'initial']]); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [, actions] = result.current; + const actions = result.current; await act(async () => { actions.set(1, 'edited'); rerender(); }); - expect(result.current[0].get(1)).toBe('edited'); + expect(result.current.map.get(1)).toBe('edited'); }); it('should replace all values with setAll', async () => { @@ -58,40 +58,40 @@ describe('useMap', () => { [2, 'example'], ]); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [, actions] = result.current; + const actions = result.current; - expect(result.current[0].get(1)).toBe('initial'); - expect(result.current[0].get(2)).toBe('example'); - expect(result.current[0].size).toBe(2); + expect(result.current.map.get(1)).toBe('initial'); + expect(result.current.map.get(2)).toBe('example'); + expect(result.current.map.size).toBe(2); await act(async () => { actions.setAll([[1, 'edited']]); rerender(); }); - expect(result.current[0].get(1)).toBe('edited'); - expect(result.current[0].get(2)).toBeUndefined(); - expect(result.current[0].size).toBe(1); + expect(result.current.map.get(1)).toBe('edited'); + expect(result.current.map.get(2)).toBeUndefined(); + expect(result.current.map.size).toBe(1); }); it('should remove an existing value from the Map', async () => { const initialMap = new Map([[1, 'initial']]); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [, actions] = result.current; + const actions = result.current; await act(async () => { actions.remove(1); rerender(); }); - expect(result.current[0].get(1)).toBeUndefined(); - expect(result.current[0].size).toBe(0); + expect(result.current.map.get(1)).toBeUndefined(); + expect(result.current.map.size).toBe(0); }); it('should reset the Map to its initial state', async () => { const initialMap = new Map([[1, 'initial']]); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [, actions] = result.current; + const actions = result.current; // First modify the map await act(async () => { @@ -100,9 +100,9 @@ describe('useMap', () => { rerender(); }); - expect(result.current[0].get(1)).toBe('modified'); - expect(result.current[0].get(2)).toBe('added'); - expect(result.current[0].size).toBe(2); + expect(result.current.map.get(1)).toBe('modified'); + expect(result.current.map.get(2)).toBe('added'); + expect(result.current.map.size).toBe(2); // Then reset to initial state await act(async () => { @@ -111,14 +111,14 @@ describe('useMap', () => { }); // Should be back to initial state - expect(result.current[0].get(1)).toBe('initial'); - expect(result.current[0].get(2)).toBeUndefined(); - expect(result.current[0].size).toBe(1); + expect(result.current.map.get(1)).toBe('initial'); + expect(result.current.map.get(2)).toBeUndefined(); + expect(result.current.map.size).toBe(1); }); it('should reset to empty Map when initialized with empty Map', async () => { const { result, rerender } = await renderHookSSR(() => useMap()); - const [, actions] = result.current; + const actions = result.current; // Add some items await act(async () => { @@ -127,7 +127,7 @@ describe('useMap', () => { rerender(); }); - expect(result.current[0].size).toBe(2); + expect(result.current.map.size).toBe(2); // Reset should restore to empty state await act(async () => { @@ -135,37 +135,37 @@ describe('useMap', () => { rerender(); }); - expect(result.current[0].size).toBe(0); + expect(result.current.map.size).toBe(0); }); it('should create a new Map reference when values change', async () => { const initialMap = new Map(); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [originalMapRef, actions] = result.current; + const { map: originalMapRef, set } = result.current; await act(async () => { - actions.set(1, 1); + set(1, 1); rerender(); }); - expect(originalMapRef).not.toBe(result.current[0]); + expect(originalMapRef).not.toBe(result.current.map); expect(originalMapRef.get(1)).toBeUndefined(); - expect(result.current[0].get(1)).toBe(1); + expect(result.current.map.get(1)).toBe(1); }); it('should maintain stable actions reference after Map changes', async () => { const initialMap = new Map(); const { result, rerender } = await renderHookSSR(() => useMap(initialMap)); - const [, originalActionsRef] = result.current; + const originalActionsRef = result.current; - expect(result.current[1]).toBe(originalActionsRef); + expect(result.current.set).toBe(originalActionsRef.set); await act(async () => { originalActionsRef.set(1, 1); rerender(); }); - expect(result.current[1]).toBe(originalActionsRef); + expect(result.current.set).toBe(originalActionsRef.set); }); it('is safe in server-side rendering', () => { diff --git a/packages/react-simplikit/src/hooks/useMap/useMap.ts b/packages/react-simplikit/src/hooks/useMap/useMap.ts index 1e60a994..03d36d3b 100644 --- a/packages/react-simplikit/src/hooks/useMap/useMap.ts +++ b/packages/react-simplikit/src/hooks/useMap/useMap.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { usePreservedReference } from '../usePreservedReference/usePreservedReference.ts'; @@ -25,7 +25,7 @@ type MapActions = { * Return type of the useMap hook. * Hides certain methods to prevent direct mutations. */ -type UseMapReturn = [Omit, 'set' | 'clear' | 'delete'>, MapActions]; +type UseMapReturn = { map: Omit, 'set' | 'clear' | 'delete'> } & MapActions; /** * @description @@ -35,11 +35,11 @@ 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) - * @returns {UseMapReturn} A tuple containing the Map state and actions to manipulate it + * @returns {UseMapReturn} An object containing the Map state and actions to manipulate it * * @example * ```tsx - * const [userMap, actions] = useMap([ + * const { map: userMap, set } = useMap([ * ['user1', { name: 'John', age: 30 }] * ]); * @@ -47,7 +47,7 @@ type UseMapReturn = [Omit, 'set' | 'clear' | 'delete'>, MapActio * const user1 = userMap.get('user1'); * * // Updating the Map - * actions.set('user2', { name: 'Jane', age: 25 }); + * set('user2', { name: 'Jane', age: 25 }); * ``` */ export function useMap(initialState: MapOrEntries = new Map()): UseMapReturn { @@ -81,9 +81,5 @@ export function useMap(initialState: MapOrEntries = new Map()): UseM setMap(() => new Map(preservedInitialState)); }, [preservedInitialState]); - const actions = useMemo>(() => { - return { set, setAll, remove, reset }; - }, [set, setAll, remove, reset]); - - return [map, actions]; + return { map, 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..746a4f09 100644 --- a/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md +++ b/packages/react-simplikit/src/hooks/useMap/zh-Hans/useMap.md @@ -22,13 +22,13 @@ function useMap(initialState: MapOrEntries): UseMapReturn; ## 示例 ```tsx -const [userMap, actions] = useMap([ +const { map: userMap, set } = useMap([ ['user1', { name: 'John', age: 30 }], ]); @@ -36,5 +36,5 @@ const [userMap, actions] = useMap([ const user1 = userMap.get('user1'); // Updating the Map -actions.set('user2', { name: 'Jane', age: 25 }); +set('user2', { name: 'Jane', age: 25 }); ``` diff --git a/packages/react-simplikit/src/hooks/useSet/es/useSet.md b/packages/react-simplikit/src/hooks/useSet/es/useSet.md index b8514301..3b4a9007 100644 --- a/packages/react-simplikit/src/hooks/useSet/es/useSet.md +++ b/packages/react-simplikit/src/hooks/useSet/es/useSet.md @@ -22,40 +22,40 @@ function useSet(initialState: SetOrValues = new Set()): UseSetReturn; @@ -94,9 +90,12 @@ function useStorageState( import { useStorageState } from 'react-simplikit'; function Counter() { - const [count, setCount] = useStorageState('counter', { - defaultValue: 0, - }); + const { value: count, setValue: setCount } = useStorageState( + 'counter', + { + defaultValue: 0, + } + ); return ( diff --git a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.spec.ts b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.spec.ts index 5bccd602..140e2fa0 100644 --- a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.spec.ts +++ b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.spec.ts @@ -149,35 +149,35 @@ describe('useStorageState', () => { it('is safe on server side rendering', () => { const result = renderHookSSR.serverOnly(() => useStorageState('test-key', { defaultValue: 'default', storage })); - const [value] = result.current; + const { value } = result.current; expect(value).toBe('default'); }); it('should initialize without default value', async () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { storage })); - expect(result.current[0]).toBeUndefined(); + expect(result.current.value).toBeUndefined(); }); it('should initialize with default value', async () => { const defaultValue = 'default'; const { result } = await renderHookSSR(() => useStorageState('test-key', { defaultValue, storage })); - expect(result.current[0]).toBe(defaultValue); + expect(result.current.value).toBe(defaultValue); }); it('should set and get value', async () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { storage })); await act(async () => { - result.current[1]('new value'); + result.current.setValue('new value'); }); - expect(result.current[0]).toBe('new value'); + expect(result.current.value).toBe('new value'); }); it('should update value using function', async () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { defaultValue: 0, storage })); await act(async () => { - result.current[1](prev => prev + 1); + result.current.setValue(prev => prev + 1); }); - expect(result.current[0]).toBe(1); + expect(result.current.value).toBe(1); }); it('should sync between multiple hooks with same key', async () => { @@ -185,10 +185,10 @@ describe('useStorageState', () => { const { result: result2 } = await renderHookSSR(() => useStorageState('test-key', { storage })); await act(async () => { - result1.current[1]('updated value'); + result1.current.setValue('updated value'); }); - expect(result2.current[0]).toBe('updated value'); + expect(result2.current.value).toBe('updated value'); }); it('should refresh storage state', async () => { @@ -197,10 +197,10 @@ describe('useStorageState', () => { storage.set('test-key', JSON.stringify({ hello: 'world' })); await act(async () => { - result.current[2](); + result.current.refresh(); }); - expect(result.current[0]).toEqual({ hello: 'world' }); + expect(result.current.value).toEqual({ hello: 'world' }); }); it('should work with custom serializer and deserializer', async () => { @@ -212,10 +212,10 @@ describe('useStorageState', () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { storage, serializer, deserializer })); await act(async () => { - result.current[1]('hello'); + result.current.setValue('hello'); }); - expect(result.current[0]).toEqual('hello'); + expect(result.current.value).toEqual('hello'); }); it('should throw error when value is not serializable', async () => { @@ -244,11 +244,11 @@ describe('useStorageState', () => { const { result, rerender } = await renderHookSSR(() => useStorageState('test-key', { storage })); await act(async () => { - result.current[1]('memo value'); + result.current.setValue('memo value'); }); rerender(); - expect(result.current[0]).toBe('memo value'); + expect(result.current.value).toBe('memo value'); }); }); @@ -266,11 +266,11 @@ describe('useStorageState', () => { ); await act(async () => { - result.current[1]('session value'); + result.current.setValue('session value'); }); rerender(); - expect(result.current[0]).toBe('session value'); + expect(result.current.value).toBe('session value'); }); it('should not sync between different tabs', async () => { @@ -287,7 +287,7 @@ describe('useStorageState', () => { ); }); - expect(result.current[0]).toBeUndefined(); + expect(result.current.value).toBeUndefined(); }); }); @@ -304,11 +304,11 @@ describe('useStorageState', () => { ); await act(async () => { - result.current[1]('local value'); + result.current.setValue('local value'); }); rerender(); - expect(result.current[0]).toBe('local value'); + expect(result.current.value).toBe('local value'); }); it('should sync between different tabs', async () => { @@ -324,7 +324,7 @@ describe('useStorageState', () => { ); }); - expect(result.current[0]).toBe('value from other tab'); + expect(result.current.value).toBe('value from other tab'); }); it.each(['default', undefined])('should reset to %s when another tab clears localStorage', async defaultValue => { @@ -332,14 +332,14 @@ describe('useStorageState', () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { storage: safeLocalStorage, defaultValue }) ); - expect(result.current[0]).toBe('stored value'); + expect(result.current.value).toBe('stored value'); await act(async () => { localStorage.clear(); window.dispatchEvent(new StorageEvent('storage', { key: null, storageArea: localStorage })); }); - expect(result.current[0]).toBe(defaultValue); + expect(result.current.value).toBe(defaultValue); }); it('should preserve localStorage state when sessionStorage is cleared', async () => { @@ -352,7 +352,7 @@ describe('useStorageState', () => { window.dispatchEvent(new StorageEvent('storage', { key: null, storageArea: sessionStorage })); }); - expect(result.current[0]).toBe('stored value'); + expect(result.current.value).toBe('stored value'); }); it('should reset to defaultValue when another tab removes the observed key', async () => { @@ -366,7 +366,7 @@ describe('useStorageState', () => { window.dispatchEvent(new StorageEvent('storage', { key: 'test-key', storageArea: localStorage })); }); - expect(result.current[0]).toBe('default'); + expect(result.current.value).toBe('default'); }); it('should preserve the observed value when another tab removes an unrelated key', async () => { @@ -379,7 +379,7 @@ describe('useStorageState', () => { window.dispatchEvent(new StorageEvent('storage', { key: 'other-key', storageArea: localStorage })); }); - expect(result.current[0]).toBe('stored value'); + expect(result.current.value).toBe('stored value'); }); it('should return defaultValue when an error occurred while parsing data', async () => { @@ -397,23 +397,23 @@ describe('useStorageState', () => { ); }); - expect(result.current[0]).toBe('default'); + expect(result.current.value).toBe('default'); }); it('should remove value when set value to undefined', async () => { const { result } = await renderHookSSR(() => useStorageState('test-key', { storage: safeLocalStorage })); await act(async () => { - result.current[1]('value'); + result.current.setValue('value'); }); - expect(result.current[0]).toBe('value'); + expect(result.current.value).toBe('value'); await act(async () => { - result.current[1](undefined); + result.current.setValue(undefined); }); - expect(result.current[0]).toBeUndefined(); + expect(result.current.value).toBeUndefined(); }); }); }); diff --git a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts index b63349d9..51098590 100644 --- a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts +++ b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts @@ -21,9 +21,15 @@ type StorageStateOptionsWithSerializer = StorageStateOptions & { deserializer: (value: string) => Serializable; }; -type SerializableGuard = T[0] extends any +type StorageStateReturn = { + value: T; + setValue: (value: SetStateAction) => void; + refresh: () => void; +}; + +type SerializableGuard = T['value'] extends any ? T - : T[0] extends never + : T['value'] extends never ? 'Received a non-serializable value' : T; @@ -49,16 +55,16 @@ function isPlainObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === '[object Object]'; } -const ensureSerializable = (value: T): SerializableGuard => { +const ensureSerializable = (result: T): SerializableGuard => { if ( - value[0] != null && - !['string', 'number', 'boolean'].includes(typeof value[0]) && - !(isPlainObject(value[0]) || Array.isArray(value[0])) + result.value != null && + !['string', 'number', 'boolean'].includes(typeof result.value) && + !(isPlainObject(result.value) || Array.isArray(result.value)) ) { throw new Error('Received a non-serializable value'); } - return value as SerializableGuard; + return result as SerializableGuard; }; /** @@ -74,43 +80,35 @@ const ensureSerializable = (value: T): SerializableGua * @param {Function} [options.serializer] - A function to serialize the state value to a string. * @param {Function} [options.deserializer] - A function to deserialize the state value from a string. * - * @returns {readonly [state: Serializable | undefined, setState: (value: SetStateAction | undefined>) => void, refreshState: () => void]} A tuple: - * - state `Serializable | undefined` - The current state value retrieved from storage; - * - setState `(value: SetStateAction | undefined>) => void` - A function to update and persist the state; - * - refreshState `() => void` - A function to refresh the state from storage; + * @returns {StorageStateReturn | undefined>} An object containing: + * - value `Serializable | undefined` - The current state value retrieved from storage; + * - setValue `(value: SetStateAction | undefined>) => void` - Updates and persists the state; + * - refresh `() => void` - Refreshes the state from storage; * @example * // Counter with persistent state * import { useStorageState } from 'react-simplikit'; * * function Counter() { - * const [count, setCount] = useStorageState('counter', { + * const { value: count, setValue: setCount } = useStorageState('counter', { * defaultValue: 0, * }); * * return ; * } */ -export function useStorageState( - key: string -): SerializableGuard< - readonly [Serializable | undefined, (value: SetStateAction | undefined>) => void, () => void] ->; +export function useStorageState(key: string): SerializableGuard | undefined>>; export function useStorageState( key: string, options: StorageStateOptionsWithDefaultValue -): SerializableGuard, (value: SetStateAction>) => void, () => void]>; +): SerializableGuard>>; export function useStorageState( key: string, options: StorageStateOptions -): SerializableGuard< - readonly [Serializable | undefined, (value: SetStateAction | undefined>) => void, () => void] ->; +): SerializableGuard | undefined>>; export function useStorageState( key: string, options: StorageStateOptionsWithSerializer -): SerializableGuard< - readonly [Serializable | undefined, (value: SetStateAction | undefined>) => void, () => void] ->; +): SerializableGuard | undefined>>; export function useStorageState( key: string, { @@ -118,9 +116,7 @@ export function useStorageState( defaultValue, ...options }: StorageStateOptions | StorageStateOptionsWithSerializer = {} -): SerializableGuard< - readonly [Serializable | undefined, (value: SetStateAction | undefined>) => void, () => void] -> { +): SerializableGuard | undefined>> { // Without `'use no memo'`, React Compiler throws when `panicThreshold` is not `'none'` // because `cache.current` is read from `getSnapshot`, which `useSyncExternalStore` calls // during render. Belongs on this implementation signature — the overload declarations @@ -195,7 +191,7 @@ export function useStorageState( /* eslint-disable-next-line react-hooks/refs -- the two callbacks close over `cache` through `getSnapshot`, so the rule treats handing them to any function as a possible ref read - during render. `ensureSerializable` only inspects element 0 — `storageState`, a plain + during render. `ensureSerializable` only inspects `value` — `storageState`, a plain value — and never calls them; passing either callback alone reproduces the report. */ - return ensureSerializable([storageState, setStorageState, refreshStorageState] as const); + return ensureSerializable({ value: storageState, setValue: setStorageState, refresh: refreshStorageState }); } diff --git a/packages/react-simplikit/src/hooks/useStorageState/zh-Hans/useStorageState.md b/packages/react-simplikit/src/hooks/useStorageState/zh-Hans/useStorageState.md index 36ded34f..d4d36170 100644 --- a/packages/react-simplikit/src/hooks/useStorageState/zh-Hans/useStorageState.md +++ b/packages/react-simplikit/src/hooks/useStorageState/zh-Hans/useStorageState.md @@ -8,11 +8,7 @@ function useStorageState( key: string, options?: Object -): readonly [ - state: Serializable | undefined, - setState: (value: SetStateAction | undefined>) => void, - refreshState: () => void, -]; +): StorageStateReturn | undefined>; ``` ### 参数 @@ -62,23 +58,23 @@ function useStorageState(