diff --git a/.axioma/quality.md b/.axioma/quality.md index 57eeb14..d81b3b1 100644 --- a/.axioma/quality.md +++ b/.axioma/quality.md @@ -77,3 +77,7 @@ ## 2026-06-08 - Distinguishing Abort Reasons in Async Components **Learning:** When using `AbortController` in components that also implement a `timeOut`, it is critical to distinguish between a manual abort (e.g., due to dependency change or unmount) and a timeout abort. Indiscriminately ignoring all aborted promises in a `.catch` block can swallow legitimate timeout errors. **Action:** Use `signal.reason` to filter aborts in `.catch` blocks: `if (signal.aborted && signal.reason !== 'Timeout') return`. This ensures race conditions are prevented while still allowing the component to transition to an error state upon timeout. + +## 2026-06-10 - [Robust Hook Input Validation] +**Learning:** React hooks that wrap timing-sensitive browser APIs (like `useThrottle`) should implement defensive checks for their numeric parameters (e.g., `limit`). Using a pattern like `if (!limit || limit <= 0)` covers `NaN`, `0`, and negative values, ensuring the hook falls back to an immediate update rather than passing invalid values to `setTimeout`. +**Action:** Always validate numeric inputs in hooks that interact with browser timers or scheduling APIs to provide a safe and predictable fallback. diff --git a/lib/hooks/useThrottle.test.ts b/lib/hooks/useThrottle.test.ts index f24ce74..6cf4a26 100644 --- a/lib/hooks/useThrottle.test.ts +++ b/lib/hooks/useThrottle.test.ts @@ -94,6 +94,30 @@ describe('useThrottle', () => { expect(result.current).toBe('updated 1') }) + it('should return the value immediately when limit is negative', () => { + const { result, rerender } = renderHook( + ({ val, limit }) => useThrottle(val, limit), + { + initialProps: { val: 'initial', limit: -100 }, + }, + ) + + rerender({ val: 'updated 1', limit: -100 }) + expect(result.current).toBe('updated 1') + }) + + it('should return the value immediately when limit is NaN', () => { + const { result, rerender } = renderHook( + ({ val, limit }) => useThrottle(val, limit), + { + initialProps: { val: 'initial', limit: NaN }, + }, + ) + + rerender({ val: 'updated 1', limit: NaN }) + expect(result.current).toBe('updated 1') + }) + it('should use default limit of 500ms when not provided', () => { const { result, rerender } = renderHook(({ val }) => useThrottle(val), { initialProps: { val: 'initial' }, diff --git a/lib/hooks/useThrottle.ts b/lib/hooks/useThrottle.ts index f4b6388..53854ef 100644 --- a/lib/hooks/useThrottle.ts +++ b/lib/hooks/useThrottle.ts @@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'react' * @template T * @param value - The value to throttle. * @param limit - The limit in milliseconds to throttle the value. + * @default 500 * @returns The throttled value. * * @example @@ -41,7 +42,7 @@ export function useThrottle(value: T, limit: number = 500): T { return } - if (limit <= 0) { + if (!limit || limit <= 0) { setThrottledValue(value) return }