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/.vitepress/theme/style.css b/apps/doc/.vitepress/theme/style.css index 377620f..9f98b07 100644 --- a/apps/doc/.vitepress/theme/style.css +++ b/apps/doc/.vitepress/theme/style.css @@ -8,7 +8,7 @@ * * Each colors have exact same color scale system with 3 levels of solid * colors with different brightness, and 1 soft color. - * + * * - `XXX-1`: The most solid color used mainly for colored text. It must * satisfy the contrast ratio against when used on top of `XXX-soft`. * @@ -128,3 +128,11 @@ .DocSearch { --docsearch-primary-color: var(--vp-c-brand-1) !important; } + +/* Dynamic color for icons in index.md */ +html.dark .icon-color-theme { + fill: #e7e7e7; +} +.icon-color-theme { + fill: currentColor; +} diff --git a/apps/doc/getting-started/overview.md b/apps/doc/getting-started/overview.md index 487f55d..739eb2a 100644 --- a/apps/doc/getting-started/overview.md +++ b/apps/doc/getting-started/overview.md @@ -1,29 +1,35 @@ # Overview -**_`classic-react-hooks`_** is a lightweight yet powerful library of custom _react-hooks_ and _components_ that streamline everyday development. +**_`classic-react-hooks`_** is a lightweight yet robust library of custom React hooks and components designed to simplify and streamline everyday development tasks. -It encourages you to write _`clean`_, _`declarative`_, _`modular`_, and _`predictable`_ code that remains easy to maintain and scale as your project grows. +It promotes _`clean`_, _`declarative`_, _`modular`_, and _`predictable`_ code, making applications easier to maintain and scale as they grow. -This library is written with Typescript having _`Type-Safety`_ in mind. It is _`Tree-Shakable`_ and `minimal` as much as possible. All of the hooks are fully compatible with _`SSR`_(no hydration mismatch) and have been tested with all possible test cases using _`Vitest`_ and _`React-Testing-Library`_ (new test cases contribution are welcomed). +Built with `TypeScript` and a strong emphasis on type safety, the library is _`minimal`_, _`tree-shakable`_, and optimized for modern React applications. All hooks are fully compatible with server-side rendering _`(SSR)`_, ensuring no hydration mismatches. + +The library is thoroughly tested using _`Vitest`_ and _`React-Testing-Library`_, covering a wide range of use cases. Contributions of additional test cases are always welcome. ## Motivation -Most of the hook libraries out there are specialized solution or packed with lots of unnecessary hooks. Some of them are just a wrapper on another mini-library. In which each hook introduces new _`syntax api`_ which mostly focuses on _`small syntax`_ and _`less lines of code`_ rather than _`Predictability`_, which results in _`Hard to remember syntax`_, _`Unpredictable API behaviours`_. And they are not so flexible either. +**_`classic-react-hooks`_** is designed to provide a _`focused`_, _`predictable`_, and _`developer-friendly`_ set of React hooks that prioritize clarity, consistency, and long-term maintainability. The library emphasizes stable APIs, minimal abstractions, and practical flexibility, allowing developers to reason about behavior with confidence while building scalable applications. + +In contrast, many existing hook libraries tend to focus on highly specialized use cases or offer broad collections of hooks that may not be universally applicable. Some primarily act as abstractions over smaller utility libraries, introducing additional layers of indirection without delivering proportional architectural value. + +These libraries often expose _`distinct APIs`_ for individual hooks, favoring _`syntactic brevity`_ over _`predictability`_ and _`consistency`_. While this can reduce boilerplate, it may also lead to APIs that are harder to internalize, less consistent in behavior, and more constrained in real-world usage. -Pretty much all of the hooks just use too much of _`useEffect`_, _`useCallback`_ and _`useMemo`_ hooks for keeping track of values, functions and attaching-cleaning up the events. And sometimes you have to yourself memoize them and do a work around to fix stale-closure values inside those callback . Which is not always the great choice. Because it can cause lots of recalculation and effect trigger resulting in heavy memory and cpu usage. +Additionally, a common pattern among such libraries is a _`heavy reliance`_ on **useEffect**, **useCallback**, and **useMemo** for state tracking, function memoization, and lifecycle management. This frequently places the burden of dependency management and _`stale-closure`_ prevention on developers, potentially resulting in unnecessary _`re-renders`_, increased computational overhead, and more complex performance tuning. -Instead of building features you often have to focus more on learning the APIs, which becomes time consuming. +As a result, development effort often shifts away from building features toward managing library-specific APIs. **_`classic-react-hooks`_** aims to minimize this cognitive load, enabling developers to focus on delivering features rather than adapting to complex or inconsistent abstractions. ## What `classic-react-hooks` offers -- Feature packed Hooks -- Performant, Minimal and Lightweight -- Predictable API Behaviours(just by using it) -- Written in Typescript (Type-Safety in mind) -- No Third Party Dependencies -- Modular and Declarative -- Tree-Shakable -- Detailed Documentation +- A thoughtfully curated set of feature-rich hooks +- High performance with a minimal and lightweight footprint +- Predictable and intuitive API behavior through natural usage +- Built with TypeScript, prioritizing strong type safety +- Zero third-party dependencies +- Modular and declarative design principles +- Fully tree-shakable for optimal bundling +- Comprehensive and well-structured documentation ## Installation 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 ( +
+