Feat/compt 30 state storage hooks#1
Conversation
…ooks - useDebounce<T>(value, delay): returns debounced value, resets timer on value/delay change - useLocalStorage<T>(key, initial): syncs with localStorage, SSR-safe, JSON serialization - useSessionStorage<T>(key, initial): same pattern for sessionStorage - Shared storage.ts helper with readStorageValue/writeStorageValue (SSR guard + parse fallback) - All three exported from src/hooks/index.ts -> src/index.ts - Full test coverage: timer reset, JSON sync, parse error fallback, SSR guard - tsc --noEmit passes, lint passes (0 warnings), 13/13 tests pass
…y pre-commit - .changeset/COMPT-30-state-storage-hooks.md: minor bump summary for 0.0.1 release - .github/instructions/copilot-instructions.md: updated to HooksKit package identity, real src structure with COMPT-30 hooks marked, COMPT-XX branch naming convention - .husky/pre-commit: removed deprecated husky v9 shebang lines (breaks in v10)
|
There was a problem hiding this comment.
Pull request overview
Adds the first “state & storage” hooks to the HooksKit package, along with shared storage helpers, tests, and public exports to expose the new hooks as part of the library API.
Changes:
- Introduces
useDebounce,useLocalStorage, anduseSessionStoragehooks undersrc/hooks/. - Adds shared SSR-guarded JSON storage helpers (
readStorageValue/writeStorageValue) and Vitest coverage for the new hooks. - Updates hook barrel exports, Copilot instructions, Husky pre-commit hook, and includes a changeset for a minor release.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/hooks/useSessionStorage.ts |
Adds sessionStorage-backed state hook using shared storage helpers. |
src/hooks/useSessionStorage.test.ts |
Adds tests for session storage behavior + SSR helper guard. |
src/hooks/useLocalStorage.ts |
Adds localStorage-backed state hook using shared storage helpers. |
src/hooks/useLocalStorage.test.ts |
Adds tests for local storage behavior + SSR helper guard. |
src/hooks/useDebounce.ts |
Adds value debouncing hook using a timeout. |
src/hooks/useDebounce.test.ts |
Adds fake-timer tests for debounce timing behavior. |
src/hooks/storage.ts |
Introduces shared JSON read/write helpers with SSR guard and error swallowing. |
src/hooks/index.ts |
Exports the new hooks from the hooks barrel. |
.husky/pre-commit |
Adjusts pre-commit hook command to run lint-staged. |
.github/instructions/copilot-instructions.md |
Updates repo/module guidance to “HooksKit” and documents SSR-safe expectations. |
.changeset/COMPT-30-state-storage-hooks.md |
Declares a minor release for adding the new hooks. |
| const timeoutId = window.setTimeout(() => { | ||
| setDebouncedValue(value); | ||
| }, delay); | ||
|
|
||
| return () => { | ||
| window.clearTimeout(timeoutId); |
There was a problem hiding this comment.
useDebounce uses window.setTimeout/window.clearTimeout. This will throw in non-DOM runtimes (e.g., React Native/tests without JSDOM) and contradicts the repo’s stated SSR-safety goal. Prefer globalThis.setTimeout/globalThis.clearTimeout (or the unqualified setTimeout/clearTimeout) so the hook doesn’t require window.
| const timeoutId = window.setTimeout(() => { | |
| setDebouncedValue(value); | |
| }, delay); | |
| return () => { | |
| window.clearTimeout(timeoutId); | |
| const timeoutId = setTimeout(() => { | |
| setDebouncedValue(value); | |
| }, delay); | |
| return () => { | |
| clearTimeout(timeoutId); |
| // Example placeholder export — replace with real hooks later. | ||
| export const __hooks_placeholder = true; | ||
|
|
There was a problem hiding this comment.
__hooks_placeholder is exported from the hooks barrel and therefore becomes part of the package’s public API via src/index.ts. Now that real hooks are present, this placeholder export should be removed to avoid shipping a permanent, undocumented API surface.
| // Example placeholder export — replace with real hooks later. | |
| export const __hooks_placeholder = true; |
| if (typeof window !== 'undefined') { | ||
| window.localStorage.clear(); | ||
| } | ||
| vi.unstubAllGlobals(); |
There was a problem hiding this comment.
afterEach clears localStorage before calling vi.unstubAllGlobals(). If a test stubs window to undefined, the clear is skipped and storage can leak into later tests. Unstub/restore globals first (or use a finally) and then clear storage to keep cleanup reliable regardless of test order.
| if (typeof window !== 'undefined') { | |
| window.localStorage.clear(); | |
| } | |
| vi.unstubAllGlobals(); | |
| vi.unstubAllGlobals(); | |
| if (typeof window !== 'undefined') { | |
| window.localStorage.clear(); | |
| } |
| if (typeof window !== 'undefined') { | ||
| window.sessionStorage.clear(); | ||
| } | ||
| vi.unstubAllGlobals(); |
There was a problem hiding this comment.
afterEach clears sessionStorage before calling vi.unstubAllGlobals(). If a test stubs window to undefined, the clear is skipped and storage can leak into later tests. Unstub/restore globals first (or use a finally) and then clear storage to keep cleanup reliable regardless of test order.
| if (typeof window !== 'undefined') { | |
| window.sessionStorage.clear(); | |
| } | |
| vi.unstubAllGlobals(); | |
| vi.unstubAllGlobals(); | |
| if (typeof window !== 'undefined') { | |
| window.sessionStorage.clear(); | |
| } |
| export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] { | ||
| const storage = typeof window === 'undefined' ? undefined : window.localStorage; |
There was a problem hiding this comment.
Accessing window.localStorage can throw (e.g., in some privacy modes or sandboxed iframes). Since this happens during render, it would crash the hook before the try/catch in readStorageValue/writeStorageValue can help. Consider wrapping the storage acquisition in a try/catch (returning undefined on failure) or centralizing it in a safe helper.
| export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] { | |
| const storage = typeof window === 'undefined' ? undefined : window.localStorage; | |
| function getLocalStorage(): Storage | undefined { | |
| if (typeof window === 'undefined') { | |
| return undefined; | |
| } | |
| try { | |
| return window.localStorage; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] { | |
| const storage = getLocalStorage(); |
| export function useSessionStorage<T>( | ||
| key: string, | ||
| initialValue: T, | ||
| ): [T, Dispatch<SetStateAction<T>>] { | ||
| const storage = typeof window === 'undefined' ? undefined : window.sessionStorage; |
There was a problem hiding this comment.
Accessing window.sessionStorage can throw (e.g., in some privacy modes or sandboxed iframes). Since this happens during render, it would crash the hook before the try/catch in readStorageValue/writeStorageValue can help. Consider wrapping the storage acquisition in a try/catch (returning undefined on failure) or centralizing it in a safe helper.
| export function useSessionStorage<T>( | |
| key: string, | |
| initialValue: T, | |
| ): [T, Dispatch<SetStateAction<T>>] { | |
| const storage = typeof window === 'undefined' ? undefined : window.sessionStorage; | |
| function getSessionStorage(): Storage | undefined { | |
| if (typeof window === 'undefined') { | |
| return undefined; | |
| } | |
| try { | |
| return window.sessionStorage; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| export function useSessionStorage<T>( | |
| key: string, | |
| initialValue: T, | |
| ): [T, Dispatch<SetStateAction<T>>] { | |
| const storage = getSessionStorage(); |



Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes