content via ref internally
const [isPending, cancelSave, restartSave] = useTimeoutFn(
async () => {
setIsSaving(true);
try {
await onSave(content);
} finally {
setIsSaving(false);
}
},
delayMs,
{ immediate: false } // Start only when explicitly triggered
);
const handleContentChange = (newContent: string) => {
// Cancel existing timer and restart with new content
cancelSave();
restartSave();
};
return (
<div>
<textarea onChange={(e) => handleContentChange(e.target.value)} />
{isPending && <span className="status">Saving in {delayMs / 1000}s...</span>}
{isSaving && <span className="status">Saving...</span>}
</div>
);
}
**Architecture Rationale:** Using `immediate: false` defers execution until `restartSave` is called. This pattern is ideal for debouncing-like behavior where the timer should only start after an event occurs. The `cancel` and `restart` methods allow the component to reset the timer on every keystroke without triggering a re-render of the effect logic.
#### 2. `useTimeout`: Delayed Rendering
`useTimeout` triggers a component re-render after a delay. It is optimized for UI patterns where content should appear only after a threshold, preventing flash-of-unwanted-content (FOUC).
**Return Signature:** `[isPending: boolean, cancel: () => void, restart: () => void]`
**Implementation Example: Deferred Image Loading**
This component delays rendering an image placeholder to avoid flicker on fast connections.
```typescript
import { useTimeout } from '@reactuses/core';
interface LazyImageProps {
src: string;
alt: string;
delayMs?: number;
}
export function LazyImage({ src, alt, delayMs = 300 }: LazyImageProps) {
const [isReady] = useTimeout(delayMs);
if (!isReady) {
return <div className="skeleton-placeholder" />;
}
return <img src={src} alt={alt} loading="lazy" />;
}
Architecture Rationale: This hook abstracts the boolean state management required for delayed rendering. If the component unmounts before the delay elapses, the timer is automatically cleared. The isReady flag drives the conditional render without manual useState or useEffect boilerplate.
3. useInterval: Robust Recurring Execution
useInterval manages recurring callbacks with built-in pause/resume capabilities. It supports passing null as the delay to stop the interval, simplifying conditional polling.
Return Signature (with controls: true): { isActive: boolean, pause: () => void, resume: () => void }
Key Features:
- Null Delay Stop: Passing
null to the delay argument clears the interval.
- Visibility Awareness: Can be combined with
document.hidden to pause execution in background tabs.
- Drift Mitigation: For time displays, use the interval to trigger re-renders and read
Date.now() in the render function to ensure accuracy.
Implementation Example: Market Data Feed
This component polls a stock price API and pauses when the tab is hidden to conserve resources.
import { useInterval } from '@reactuses/core';
import { useEffect, useState } from 'react';
interface StockTickerProps {
symbol: string;
pollIntervalMs: number;
}
export function StockTicker({ symbol, pollIntervalMs }: StockTickerProps) {
const [price, setPrice] = useState<number | null>(null);
const [isPaused, setIsPaused] = useState(false);
const fetchPrice = async () => {
const response = await fetch(`/api/stocks/${symbol}`);
const data = await response.json();
setPrice(data.price);
};
// Interval runs only when active and not paused
useInterval(fetchPrice, isPaused ? null : pollIntervalMs);
// Pause on visibility change
useEffect(() => {
const handleVisibility = () => {
setIsPaused(document.hidden);
};
window.addEventListener('visibilitychange', handleVisibility);
return () => window.removeEventListener('visibilitychange', handleVisibility);
}, []);
return (
<div>
<h3>{symbol}: {price !== null ? `$${price.toFixed(2)}` : 'Loading...'}</h3>
{isPaused && <span className="badge">Paused (Tab Hidden)</span>}
</div>
);
}
Architecture Rationale: The isPaused ? null : pollIntervalMs pattern cleanly stops the interval without conditional effect logic. The visibility listener ensures the polling halts when the user switches tabs, preventing burst fires and reducing server load.
4. useCountDown: Formatted Time Remaining
useCountDown manages countdown logic, including formatting, expiry callbacks, and clamping. It returns zero-padded time segments.
Return Signature: [hours: string, minutes: string, seconds: string]
Implementation Example: Flash Sale Timer
This component displays a countdown to a sale end and triggers an action upon expiry.
import { useCountDown } from '@reactuses/core';
interface FlashSaleProps {
durationSeconds: number;
onSaleEnd: () => void;
}
export function FlashSale({ durationSeconds, onSaleEnd }: FlashSaleProps) {
const [hours, minutes, seconds] = useCountDown(
durationSeconds,
undefined,
onSaleEnd
);
const isExpired = hours === '00' && minutes === '00' && seconds === '00';
return (
<div className="sale-banner">
{isExpired ? (
<button onClick={onSaleEnd}>Sale Ended - Refresh</button>
) : (
<span>Ends in {hours}:{minutes}:{seconds}</span>
)}
</div>
);
}
Architecture Rationale: The hook handles the interval, state updates, and formatting internally. It clamps values to prevent overflow and ensures the callback fires exactly once at zero. This eliminates the need for manual date math and interval management in the component.
5. useRafFn: High-Frequency Animation
useRafFn wraps requestAnimationFrame, synchronizing callbacks with the display refresh rate. It is essential for animations and high-frequency DOM updates.
Return Signature: [stop: () => void, start: () => void, isActive: boolean]
Key Features:
- Timestamp: Callback receives a high-resolution timestamp.
- Frame Sync: Automatically skips frames if the tab is hidden or the device is throttled.
- DOM Efficiency: Best used with refs for direct DOM manipulation to avoid React render overhead.
Implementation Example: Scroll Progress Bar
This component updates a progress bar based on scroll position using requestAnimationFrame.
import { useRafFn } from '@reactuses/core';
import { useRef, useEffect } from 'react';
export function ScrollProgress() {
const barRef = useRef<HTMLDivElement>(null);
useRafFn((timestamp) => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
if (barRef.current) {
barRef.current.style.width = `${progress}%`;
}
});
return (
<div className="progress-container">
<div ref={barRef} className="progress-bar" />
</div>
);
}
Architecture Rationale: Using useRafFn ensures the progress bar updates smoothly at 60fps (or the device's refresh rate). Direct DOM manipulation via barRef.current.style bypasses React's reconciliation, reducing layout thrashing and improving performance. The hook automatically handles cleanup and frame synchronization.
Pitfall Guide
Avoid these common mistakes when implementing timers in React. Each pitfall includes a diagnosis and a remediation strategy.
-
Stale Closure in Callbacks
- Explanation: A timer callback captures state from the render in which it was created. If state changes before the timer fires, the callback uses outdated values.
- Fix: Use
@reactuses/core hooks, which internally store callbacks in refs to always access the latest scope. If writing manually, use useRef to hold the callback.
-
Interval Drift in Clocks
- Explanation:
setInterval does not guarantee precise timing. Over time, the accumulated delay causes the displayed time to lag behind the actual wall clock.
- Fix: Use
useInterval only to trigger re-renders. Read Date.now() inside the render function to calculate the current time. This decouples scheduling accuracy from display correctness.
-
Zombie Intervals in Background Tabs
- Explanation: Intervals continue running in background tabs, consuming CPU and network quota. Browsers may throttle these, causing a burst of executions when the tab becomes visible.
- Fix: Use
useInterval with a visibility listener to pause execution when document.hidden is true. Alternatively, pass null to the delay argument to stop the interval.
-
Timer Restart on Prop Change
- Explanation: Including timer parameters in a
useEffect dependency array causes the effect to re-run, resetting the timer on every prop change. This breaks user expectations for timers that should persist.
- Fix: Use
useTimeoutFn or useInterval, which maintain timer state across renders. Use restart or cancel methods to explicitly control the timer lifecycle.
-
Animation Jank with setInterval
- Explanation: Using
setInterval for animations results in inconsistent frame rates and jank, as it does not synchronize with the display refresh rate.
- Fix: Use
useRafFn for all animation logic. It synchronizes with requestAnimationFrame, ensuring smooth updates and automatic pausing when the tab is hidden.
-
Memory Leaks from Missing Cleanup
- Explanation: Forgetting to clear timers on component unmount leads to memory leaks and errors when callbacks attempt to update unmounted components.
- Fix:
@reactuses/core hooks automatically handle cleanup on unmount. If writing manually, ensure clearTimeout or clearInterval is called in the useEffect cleanup function.
-
State Overload in High-Frequency Updates
- Explanation: Calling
setState inside useRafFn or high-frequency intervals triggers excessive re-renders, degrading performance.
- Fix: Use refs for direct DOM manipulation in high-frequency loops. Reserve
setState for updates that require React reconciliation.
Production Bundle
Action Checklist
Decision Matrix
Use this matrix to select the appropriate hook based on your requirements.
| Scenario | Recommended Hook | Why | Cost Impact |
|---|
| One-off delay with cancel/restart | useTimeoutFn | Provides explicit control over timer lifecycle. | Low |
| Delayed UI rendering | useTimeout | Simplifies boolean state for deferred content. | Low |
| Recurring task with pause/resume | useInterval | Built-in controls and null-delay stop. | Low |
| Countdown with formatting | useCountDown | Handles formatting, expiry, and clamping. | Low |
| High-frequency animation | useRafFn | Synchronizes with refresh rate; avoids jank. | Low |
| Conditional polling | useInterval | Pass null to stop; integrates with visibility. | Low |
Configuration Template
This template demonstrates a robust useInterval setup with visibility handling and error recovery.
import { useInterval } from '@reactuses/core';
import { useEffect, useState, useCallback } from 'react';
interface PollingConfig {
url: string;
intervalMs: number;
onError?: (error: Error) => void;
}
export function usePolling({ url, intervalMs, onError }: PollingConfig) {
const [data, setData] = useState<any>(null);
const [isPaused, setIsPaused] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
const errorObj = err instanceof Error ? err : new Error('Unknown error');
setError(errorObj);
onError?.(errorObj);
}
}, [url, onError]);
// Interval runs only when active and not paused
useInterval(fetchData, isPaused ? null : intervalMs);
// Pause on visibility change
useEffect(() => {
const handleVisibility = () => {
setIsPaused(document.hidden);
};
window.addEventListener('visibilitychange', handleVisibility);
return () => window.removeEventListener('visibilitychange', handleVisibility);
}, []);
return { data, error, isPaused };
}
Quick Start Guide
- Install the Library:
npm install @reactuses/core
- Import the Hook:
import { useTimeoutFn, useInterval, useCountDown, useRafFn } from '@reactuses/core';
- Replace Manual Timers:
Identify
useEffect blocks with setTimeout or setInterval. Replace them with the appropriate hook, passing the callback and delay.
- Leverage Controls:
Use the returned control functions (
cancel, restart, pause, resume) to manage timer lifecycle based on user interaction or visibility changes.
- Test Edge Cases:
Verify behavior on prop changes, unmount, and tab visibility switches. Ensure no stale closures or memory leaks occur.