From 47b83cddf53e73143dcc5e7fca57c9b5647073a1 Mon Sep 17 00:00:00 2001 From: ashish-simpleCoder Date: Wed, 21 Jan 2026 23:06:28 +0530 Subject: [PATCH 1/2] feat: Introduce new api and props for use-debounced-fn New api and props for use-debounced-fn - Introduce cleanup and debouncedFn return value - Introduce immediateCallback, onSuccess, onError and onFinally callback as lifecycle functions --- .changeset/eight-webs-look.md | 7 + apps/doc/hooks/use-debounced-fn.md | 420 ++++++++++- src/index.tsx | 2 +- src/lib/use-debounced-fn/index.test.tsx | 931 ++++++++++++++++++++++-- src/lib/use-debounced-fn/index.tsx | 106 ++- 5 files changed, 1356 insertions(+), 110 deletions(-) create mode 100644 .changeset/eight-webs-look.md diff --git a/.changeset/eight-webs-look.md b/.changeset/eight-webs-look.md new file mode 100644 index 0000000..59c11c1 --- /dev/null +++ b/.changeset/eight-webs-look.md @@ -0,0 +1,7 @@ +--- +'classic-react-hooks': minor +--- + +New api and props for use-debounced-fn +- Introduce cleanup and debouncedFn return value +- Introduce immediateCallback, onSuccess, onError and onFinally callback as lifecycle functions diff --git a/apps/doc/hooks/use-debounced-fn.md b/apps/doc/hooks/use-debounced-fn.md index 0c4a926..7260d4d 100644 --- a/apps/doc/hooks/use-debounced-fn.md +++ b/apps/doc/hooks/use-debounced-fn.md @@ -4,14 +4,79 @@ outline: deep # use-debounced-fn -A React hook that returns a debounced version of any function, delaying its execution until after a specified delay has passed since the last time it was invoked. +An async aware React hook with features like built-in error handling and success/finally callbacks which completely transforms the way you implement debouncing feature in your application. ## Features - **Auto cleanup:** Timeouts are automatically cleared on unmount or dependency changes - **Flexible delay:** Configurable delay with sensible defaults -- **Performance Optimized:** Prevents excessive function calls during rapid user interactions -- **Error handling:** Preserves original function's error behavior +- **Immediate Callback:** Execute synchronous logic immediately before debouncing +- **Success Callback:** Run callback after debounced function completes successfully (supports async) +- **Error Callback:** Handle errors from debounced function execution +- **Finally Callback:** Execute cleanup logic that runs regardless of success or failure +- **Manual Cleanup:** Exposed cleanup function for advanced control +- **Type-safe Overloads:** Full TypeScript support for events and multiple arguments + +## Execution Flow for the callbacks + +```tsx +function SearchInput() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + + const { debouncedFn } = useDebouncedFn({ + /* searchTerm is type-safe for all of the callbacks. */ + immediateCallback: (searchTerm) => { + setQuery(searchTerm) /* Update UI immediately */ + }, + callbackToBounce: async (searchTerm) => { + // No try-catch needed + // Error is handled within `onError` callback + const response = await fetch(`/api/search?q=${searchTerm}`) + const data = await response.json() + setResults(data.results) + }, + onSuccess: (searchTerm) => { + console.log('runs after successful completion of callbackToBounce') + }, + onError: (error, searchTerm) => { + console.log('runs if error occurs', error) + }, + onFinally: (searchTerm) => { + console.log('runs after all of the callbacks') + }, + }) + + /* debouncedFn is aware of its type. String argument must be provided. */ + return debouncedFn(e.target.value)} placeholder='Search...' /> +} +``` + +## Callback Execution Order + +The callbacks execute in the following order: + +### Success Flow + +1. **immediateCallback** - Executes synchronously when `debouncedFn` is called +2. **callbackToBounce** - Executes after the delay period +3. **onSuccess** - Executes after `callbackToBounce` completes successfully +4. **onFinally** - Executes after `onSuccess` + +### Error Flow + +1. **immediateCallback** - Executes synchronously when `debouncedFn` is called +2. **callbackToBounce** - Executes after the delay period and throws an error +3. **onError** - Executes when error is caught (receives the error and all arguments) +4. **onFinally** - Executes after `onError` + +::: tip + +- `onSuccess` and `onError` are mutually exclusive - only one will run per execution +- `onFinally` always runs, regardless of success or error +- All callbacks except `onError` receive the same arguments passed to `debouncedFn` +- `onError` receives the error as the first argument, followed by the original arguments + ::: ::: tip The `debounced function` is purely ref based and does not change across re-renders. @@ -19,11 +84,11 @@ The `debounced function` is purely ref based and does not change across re-rende ## Problem It Solves -::: details **Boilerplate Reduction** +::: details **Boilerplate Reduction and More control on behaviors** --- -**Problem:-** Manually implementing debouncing in React components leads to lengthy, error-prone code with potential memory leaks and stale closures. +**Problem:-** Manually implementing debouncing in React components leads to less control on behaviors, lengthy, error-prone code with potential memory leaks and stale closures. ```tsx // ❌ Problematic approach which is redundant and lengthy @@ -73,35 +138,41 @@ function SearchInput() { **Solution:-** - Eliminates repetitive debounce timing logic +- Eliminates manual management of custom callback for updating the UI state +- Providing full flexibility on debouncing life cycle behavior with `immediateCallback`, `onSuccess`, `onError`, `onFinally` and `callbackToBounce` functions. - Automatic cleanup ensures timeouts are cleared when: - Component unmounts - Delay value changes - - Function reference changes -```tsx +```tsx {8,11,18,21,24} // ✅ Clean, declarative approach function SearchInput() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) - const debouncedSearch = useDebouncedFn({ - callbackToBounce: async (searchTerm: string) => { + const { debouncedFn } = useDebouncedFn({ + /* searchTerm is type-safe for all of the callbacks. */ + immediateCallback: (searchTerm) => { + setQuery(searchTerm) // Update UI immediately + }, + callbackToBounce: async (searchTerm) => { if (searchTerm.trim()) { const response = await fetch(`/api/search?q=${searchTerm}`) const data = await response.json() setResults(data.results) } }, + onSuccess: (searchTerm) => { + console.log('runs after successful completion of callbackToBounce') + }, + onError: (error, searchTerm) => { + console.error('Search failed:', error) + }, delay: 500, }) - const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setQuery(value) - debouncedSearch(value) - } - - return + /* debouncedFn is aware of its type. String argument must be provided. */ + return debouncedFn(e.target.value)} placeholder='Search...' /> } ``` @@ -112,15 +183,22 @@ function SearchInput() { - **Reduces execution frequency:** Limits function calls during rapid user input - **Memory efficient:** Proper cleanup prevents memory leaks from pending timeouts - **Stable references:** Function reference remains stable across re-renders +- **Immediate UI updates:** `immediateCallback` ensures responsive user experience +- **Async-aware:** `onSuccess` waits for async operations to complete +- **Error resilience:** `onError` handles failures gracefully without breaking the UI ::: ## Parameters -| Parameter | Type | Required | Default Value | Description | -| ---------------- | :------------------------------: | :------: | :-----------: | ----------------------------------------------- | -| callbackToBounce | [DebouncedFn](#type-definitions) | ✅ | - | The function to debounce | -| delay | number | ❌ | 300ms | Delay in milliseconds before function execution | +| Parameter | Type | Required | Default Value | Description | +| ----------------- | :------------------------------: | :------: | :-----------: | ------------------------------------------------------------------- | +| callbackToBounce | [DebouncedFn](#type-definitions) | ✅ | - | The function to debounce | +| immediateCallback | [DebouncedFn](#type-definitions) | ❌ | - | Function to execute immediately before debouncing starts | +| onSuccess | [DebouncedFn](#type-definitions) | ❌ | - | Function to execute after debounced callback completes successfully | +| onError | [ErrorFn](#type-definitions) | ❌ | - | Function to execute when debounced callback throws an error | +| onFinally | [DebouncedFn](#type-definitions) | ❌ | - | Function to execute after completion (success or error) | +| delay | number | ❌ | 300ms | Delay in milliseconds before function execution | ### Type Definitions @@ -128,28 +206,73 @@ function SearchInput() { ```ts export type DebouncedFn any> = (...args: Parameters) => void +export type ErrorFn any> = (error: Error, ...args: Parameters) => void + +// Function overloads for type safety +export function useDebouncedFn({ + immediateCallback, + callbackToBounce, + onSuccess, + onError, + onFinally, + delay, +}: { + immediateCallback?: (...args: any[]) => void + callbackToBounce: (...args: any[]) => void + onSuccess?: (...args: any[]) => void + onError?: (error: Error, ...args: any[]) => void + onFinally?: (...args: any[]) => void + delay?: number +}): { + debouncedFn: (...args: any[]) => void + cleanup: () => void +} + +// Overload with event and additional arguments +export function useDebouncedFn({ + immediateCallback, + callbackToBounce, + onSuccess, + onError, + onFinally, + delay, +}: { + immediateCallback?: (ev: Ev, ...args: Args) => void + callbackToBounce: (ev: Ev, ...args: Args) => void + onSuccess?: (ev: Ev, ...args: Args) => void + onError?: (error: Error, ev: Ev, ...args: Args) => void + onFinally?: (ev: Ev, ...args: Args) => void + delay?: number +}): { + debouncedFn: (ev: Ev, ...args: Args) => void + cleanup: () => void +} ``` ::: ## Return Value(s) -The hook returns a debounced version of the provided callback. +The hook returns an object with the debounced function and a cleanup function. | Return Value | Type | Description | | ------------- | ---------------------------------- | --------------------------------------------------------------------------------------- | | `debouncedFn` | `(...args: Parameters) => void` | Debounced version of the original function that delays execution by the specified delay | +| `cleanup` | `() => void` | Manual cleanup function to clear pending timeouts | ## Common Use Cases -- **Search functionality:** Debouncing search queries to reduce API calls -- **API rate limiting:** Preventing excessive API requests +- **Search functionality:** Debouncing search queries to reduce API calls with immediate UI updates and error handling +- **API rate limiting:** Preventing excessive API requests with proper error handling +- **Form validation:** Debouncing validation with loading states and error feedback +- **Auto-save:** Debouncing save operations with completion callbacks and error recovery +- **Resize/scroll handlers:** Optimizing expensive DOM operations with error boundaries ## Usage Examples -### Basic Search Debouncing +### Basic Search with Immediate UI Update -```tsx {8-19} +```tsx {8-21} import { useState } from 'react' import { useDebouncedFn } from 'classic-react-hooks' @@ -157,7 +280,10 @@ export default function SearchExample() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) - const debouncedSearch = useDebouncedFn({ + const { debouncedFn } = useDebouncedFn({ + immediateCallback: (searchTerm: string) => { + setQuery(searchTerm) // Update input immediately + }, callbackToBounce: async (searchTerm: string) => { if (searchTerm.trim()) { const response = await fetch(`https://api.example.com/search?q=${searchTerm}`) @@ -170,15 +296,146 @@ export default function SearchExample() { delay: 500, }) - const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setQuery(value) - debouncedSearch(value) + return ( +
+ debouncedFn(e.target.value)} placeholder='Search products...' /> +
+ {results.map((result) => ( +
{result.name}
+ ))} +
+
+ ) +} +``` + +### Auto-save with Loading State and Error Handling + +```tsx {7-34} +import { useState } from 'react' +import { useDebouncedFn } from 'classic-react-hooks' + +export default function AutoSaveEditor() { + const [content, setContent] = useState('') + const [isSaving, setIsSaving] = useState(false) + const [lastSaved, setLastSaved] = useState(null) + const [error, setError] = useState(null) + + const { debouncedFn } = useDebouncedFn({ + immediateCallback: (text: string) => { + setContent(text) // Update editor immediately + setError(null) // Clear previous errors + }, + callbackToBounce: async (text: string) => { + setIsSaving(true) + const response = await fetch('/api/save', { + method: 'POST', + body: JSON.stringify({ content: text }), + }) + if (!response.ok) throw new Error('Save failed') + }, + onSuccess: () => { + setLastSaved(new Date()) + }, + onError: (err) => { + setError(err.message) + }, + onFinally: () => { + setIsSaving(false) + }, + delay: 1000, + }) + + return ( +
+