Feat/compt 30 state storage hooks#6
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 for the HooksKit library, plus supporting storage utilities, tests, and documentation/metadata updates.
Changes:
- Introduces
useDebounce,useLocalStorage, anduseSessionStoragehooks with Vitest coverage. - Adds shared
storage.tshelpers for JSON (de)serialization with SSR guards. - Updates hook barrel exports, Changeset metadata, Husky hook config, and contributor instructions.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/hooks/useSessionStorage.ts |
New hook for sessionStorage-backed state using shared storage helpers. |
src/hooks/useSessionStorage.test.ts |
Tests for session storage behavior and fallback scenarios. |
src/hooks/useLocalStorage.ts |
New hook for localStorage-backed state using shared storage helpers. |
src/hooks/useLocalStorage.test.ts |
Tests for local storage behavior and fallback scenarios. |
src/hooks/useDebounce.ts |
New debounce hook implemented with a timeout-based effect. |
src/hooks/useDebounce.test.ts |
Tests covering debounce timing behavior with fake timers. |
src/hooks/storage.ts |
New shared helpers to read/write JSON values with SSR/error guards. |
src/hooks/index.ts |
Exports the newly added hooks from the hooks barrel. |
.husky/pre-commit |
Modifies the pre-commit hook to run lint-staged. |
.github/instructions/copilot-instructions.md |
Updates project guidelines/docs for the new HooksKit focus. |
.changeset/COMPT-30-state-storage-hooks.md |
Adds a Changeset entry describing the new hooks release. |
| @@ -0,0 +1,21 @@ | |||
| --- | |||
| "@ciscode/reactts-developerkit": minor | |||
There was a problem hiding this comment.
The changeset frontmatter targets "@ciscode/reactts-developerkit", but package.json declares the package name as @ciscode/hooks-kit. With the wrong package name, Changesets won’t version/release the actual package. Update the changeset to reference the real package name.
| "@ciscode/reactts-developerkit": minor | |
| "@ciscode/hooks-kit": minor |
|
|
||
| **Package**: `@ciscode/ui-components` (example) | ||
| **Type**: React Component Library | ||
| **Package**: `@ciscode/reactts-developerkit` |
There was a problem hiding this comment.
This doc states the package name is @ciscode/reactts-developerkit, but package.json currently uses @ciscode/hooks-kit. Please align the documentation with the actual published package name to avoid confusing consumers/contributors.
| **Package**: `@ciscode/reactts-developerkit` | |
| **Package**: `@ciscode/hooks-kit` |
| writeStorageValue(storage, key, storedValue); | ||
| }, [key, storedValue, storage]); |
There was a problem hiding this comment.
When key changes, the hook keeps the previous storedValue (since the useState initializer only runs once) but the effect will start writing that old value under the new key. This can overwrite an existing value for the new key and never re-read it. Either document that key must be stable, or add logic to re-read/reset state when key changes (and avoid overwriting until after the re-read).
| writeStorageValue(storage, key, storedValue); | |
| }, [key, storedValue, storage]); | |
| const value = readStorageValue(storage, key, initialValue); | |
| setStoredValue(value); | |
| }, [key, storage, initialValue]); | |
| useEffect(() => { | |
| writeStorageValue(storage, key, storedValue); | |
| }, [storedValue, storage]); |
| const [storedValue, setStoredValue] = useState<T>(() => { | ||
| return readStorageValue(storage, key, initialValue); | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| writeStorageValue(storage, key, storedValue); | ||
| }, [key, storedValue, storage]); |
There was a problem hiding this comment.
If key changes over time, the hook won’t re-read sessionStorage for the new key (the useState initializer runs once), but the effect will write the existing state to the new key. That can overwrite a pre-existing value for the new key. Consider either enforcing/documenting a stable key, or syncing state on key changes before writing.
| #!/usr/bin/env sh | ||
| . "$(dirname -- "$0")/_/husky.sh" | ||
|
|
||
| npx lint-staged |
There was a problem hiding this comment.
.husky/pre-commit no longer has a shebang or Husky bootstrap (. "$(dirname -- "$0")/_/husky.sh"). Git executes hooks as standalone programs; without a shebang this commonly fails with an “exec format error” and the hook won’t run. Restore the standard Husky header so the hook is executable and runs reliably across environments.
| 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 breaks the repo’s SSR-safety convention (all window access must be guarded) and also fails in non-browser React environments where window is undefined. Prefer globalThis.setTimeout/globalThis.clearTimeout or the unqualified setTimeout/clearTimeout (and/or guard with typeof window === 'undefined'). See .github/instructions/copilot-instructions.md (SSR-safe rule).
| const timeoutId = window.setTimeout(() => { | |
| setDebouncedValue(value); | |
| }, delay); | |
| return () => { | |
| window.clearTimeout(timeoutId); | |
| const timeoutId = setTimeout(() => { | |
| setDebouncedValue(value); | |
| }, delay); | |
| return () => { | |
| clearTimeout(timeoutId); |
| it('returns initial value when window is undefined (SSR guard)', () => { | ||
| vi.stubGlobal('window', undefined); | ||
|
|
||
| const value = readStorageValue(undefined, 'ssr', 'fallback'); | ||
| expect(value).toBe('fallback'); | ||
| }); |
There was a problem hiding this comment.
This test is labeled as verifying the hook’s SSR guard, but it never calls useLocalStorage; it only tests readStorageValue. That leaves useLocalStorage’s SSR behavior untested and can let regressions slip through. Either rename/move this test to a storage helper test suite, or add a test that renders useLocalStorage with window undefined and asserts it returns the initial value without throwing.
| it('returns initial value when window is undefined (SSR guard)', () => { | ||
| vi.stubGlobal('window', undefined); | ||
|
|
||
| const value = readStorageValue(undefined, 'ssr', 'fallback'); | ||
| expect(value).toBe('fallback'); | ||
| }); |
There was a problem hiding this comment.
This test is labeled as verifying the hook’s SSR guard, but it never calls useSessionStorage; it only tests readStorageValue. That means useSessionStorage’s SSR behavior isn’t actually covered. Either rename/move this test to a storage helper test suite, or add a test that renders useSessionStorage with window undefined and asserts it returns the initial value without throwing.



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