Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .axioma/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
24 changes: 24 additions & 0 deletions lib/hooks/useThrottle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
3 changes: 2 additions & 1 deletion lib/hooks/useThrottle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,7 +42,7 @@ export function useThrottle<T>(value: T, limit: number = 500): T {
return
}

if (limit <= 0) {
if (!limit || limit <= 0) {
setThrottledValue(value)
return
}
Expand Down
Loading