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 @@ -101,3 +101,7 @@
## 2024-07-10 - [Initial Mount Optimization and Robust Input Validation in Hooks]
**Learning:** React hooks managing timing-sensitive side effects (like `useDebounce` or `useThrottle`) should use an `isFirstRun` ref to skip redundant `setTimeout` calls on initial mount, as the state is already initialized with the starting value. Additionally, implementing defensive checks for numeric parameters (e.g., `delay <= 0` or `NaN`) ensures the hook falls back to an immediate update, providing a safe and predictable behavior across different edge cases.
**Action:** Always use an `isFirstRun` guard for initialization-safe side effects and validate numeric inputs to provide immediate fallbacks for invalid or non-positive values.

## 2024-07-15 - [Synchronous Error Handling in Async Components]
**Learning:** Wrapping a function call in `Promise.resolve().then()` introduces a micro-task delay. This can break tests that expect the initial state of a component (like `pending`) to be set synchronously upon mount. For components managing async operations, using a `try...catch` around the initial function call and converting synchronous errors into a rejected promise (`promise = Promise.reject(e)`) ensures both sync and async errors are handled by the same pipeline without breaking synchronous test expectations.
**Action:** Use `try...catch` around initial calls to functions expected to return promises to robustly handle synchronous exceptions without introducing micro-task delays.
21 changes: 21 additions & 0 deletions lib/components/AsyncBlock/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,25 @@ describe('<AsyncBlock />', () => {

expect(onError).not.toHaveBeenCalled()
})

it('should handle synchronous errors in promiseFn', async () => {
const error = new Error('Sync error')
const onError = vi.fn()
const errorContent = 'Error'

render(
<AsyncBlock
promiseFn={() => {
throw error
}}
pending="Loading"
success={() => null}
error={() => errorContent}
onError={onError}
/>,
)

await screen.findByText(errorContent)
expect(onError).toHaveBeenCalledWith(error)
})
})
17 changes: 9 additions & 8 deletions lib/components/AsyncBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
useEffect,
useState,
ReactNode,
useRef,
useCallback,
} from 'react'
import { useEffect, useState, ReactNode, useRef, useCallback } from 'react'

/**
* AsyncBlock component – Declaratively renders asynchronous content with pending, success, and error states.
Expand Down Expand Up @@ -94,7 +88,14 @@ export const AsyncBlock = <T,>({
}

// Pass the signal to the promise function, but make it optional
promiseFn(signal)
let promise: Promise<T>
try {
promise = promiseFn(signal)
} catch (e) {
promise = Promise.reject(e)
}

promise
.then((result) => {
if (signal.aborted) return

Expand Down
Loading