Package Exports
- use-time-hooks
Readme
use-time-hooks
A comprehensive collection of React hooks for time-based operations
A powerful, TypeScript-first library providing several production-ready React hooks for managing time-based operations, including intervals, timeouts, debouncing, throttling, stopwatches, retries, and more.
Table of Contents
- Installation
- Features
- Hooks
- useInterval - Execute functions at regular intervals
- useTimeout - Execute functions after a delay
- useDebounce - Debounce values
- useThrottle - Throttle function calls
- useStopwatch - Stopwatch with lap timing
- useRetry - Retry failed operations
- useBatchedUpdates - Batch multiple updates
- useDelayedState - State with delayed updates
- useSequentialExecution - Sequential function execution
- TypeScript Support
- Quick Start
- Examples
- Browser Compatibility
- License
Installation
npm install use-time-hooksyarn add use-time-hookspnpm add use-time-hooksFeatures
- âąī¸ useInterval - Execute functions at regular intervals with full control
- â° useTimeout - Execute functions after a delay with pause/resume
- đ useDebounce - Debounce values to limit update frequency
- đ useThrottle - Throttle function calls to improve performance
- â˛ī¸ useStopwatch - Full-featured stopwatch with lap timing
- đ useRetry - Retry failed operations with exponential backoff
- đĻ useBatchedUpdates - Batch multiple updates to reduce re-renders
- âŗ useDelayedState - State with delayed updates for optimistic UI
- đŦ useSequentialExecution - Execute functions sequentially with delays
- đ TypeScript Support - Full TypeScript support with type definitions
- ⥠Tree Shakeable - Import only what you need
- đĒļ Lightweight - Minimal bundle size impact
- â Well Tested - Comprehensive test coverage with Vitest
Hooks
useInterval
Execute a callback function at specified intervals with manual control.
import { useInterval } from 'use-time-hooks';
import { useState } from 'react';
function Timer() {
const [count, setCount] = useState(0);
const { toggle, stop, isRunning, executionCount } = useInterval(() => {
setCount((count) => count + 1);
}, 1000);
const handleReset = () => {
setCount(0);
stop();
};
return (
<div>
<p>Count: {count}</p>
<p>Executions: {executionCount}</p>
<p>Status: {isRunning ? 'Running' : 'Stopped'}</p>
<button onClick={toggle}>{isRunning ? 'Pause' : 'Start'}</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}Parameters:
callback: () => void- Function to execute at each intervaldelay: number- Delay in milliseconds between executionsimmediate?: boolean- Execute immediately on mount (default: false)
Returns:
start: () => void- Start the intervalstop: () => void- Stop the intervalreset: () => void- Reset execution count and stoptoggle: () => void- Toggle between start and stopisRunning: boolean- Current running stateexecutionCount: number- Number of times the callback has been executed
useTimeout
Execute a callback function after a specified delay with full control and real-time progress tracking.
import { useTimeout } from 'use-time-hooks';
import { useState } from 'react';
function DelayedMessage() {
const [message, setMessage] = useState('');
const { start, pause, reset, toggle, isRunning, timeRemaining } = useTimeout(
() => {
setMessage('Hello after 3 seconds!');
},
3000,
false
); // false = don't auto-start
return (
<div>
<p>{message}</p>
<p>Time remaining: {(timeRemaining / 1000).toFixed(1)}s</p>
<p>Status: {isRunning ? 'Running' : 'Stopped'}</p>
<button onClick={toggle}>{isRunning ? 'Pause' : 'Start'}</button>
<button onClick={reset}>Reset</button>
</div>
);
}Parameters:
callback: () => void- Function to execute after delaydelay: number- Delay in millisecondsautoStart?: boolean- Whether to start immediately (default: true)
Returns:
start: () => void- Start the timeoutpause: () => void- Pause the timeout (preserves remaining time)reset: () => void- Reset to initial delayclear: () => void- Clear the timeout (alias for reset)toggle: () => void- Toggle between running and pausedisRunning: boolean- Current running statetimeRemaining: number- Milliseconds remaining until executiontimeElapsed: number- Milliseconds elapsed since start
useDebounce
Debounce a value, delaying updates until after the specified delay.
import { useDebounce } from 'use-time-hooks';
import { useState, useEffect } from 'react';
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search API call
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}Parameters:
value: T- Value to debouncedelay: number- Delay in milliseconds
Returns:
T- The debounced value
useThrottle
Throttle a function, ensuring it's called at most once per specified delay.
import { useThrottle } from 'use-time-hooks';
import { useState, useEffect } from 'react';
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const throttledScroll = useThrottle(() => {
setScrollY(window.scrollY);
}, 100);
useEffect(() => {
window.addEventListener('scroll', throttledScroll);
return () => window.removeEventListener('scroll', throttledScroll);
}, [throttledScroll]);
return <div>Scroll position: {scrollY}px</div>;
}Parameters:
callback: T- Function to throttledelay: number- Minimum delay between calls in milliseconds
Returns:
T- The throttled function
useStopwatch
A full-featured stopwatch with lap timing capabilities and formatted time display.
import { useStopwatch } from 'use-time-hooks';
function StopwatchDemo() {
const {
start,
stop,
reset,
toggle,
lap,
clearLaps,
isRunning,
elapsedTime,
lapTimes,
formattedTime,
} = useStopwatch();
return (
<div>
<h1>{formattedTime}</h1>
<p>Status: {isRunning ? 'Running' : 'Stopped'}</p>
<button onClick={toggle}>{isRunning ? 'Stop' : 'Start'}</button>
<button onClick={reset}>Reset</button>
<button onClick={lap} disabled={!isRunning}>
Lap
</button>
<button onClick={clearLaps}>Clear Laps</button>
<div>
<h3>Lap Times:</h3>
{lapTimes.map((lapTime) => (
<div key={lapTime.lapNumber}>
Lap {lapTime.lapNumber}: {(lapTime.lapTime / 1000).toFixed(2)}s
(Split: {(lapTime.splitTime / 1000).toFixed(2)}s)
</div>
))}
</div>
</div>
);
}Parameters:
precision?: number- Update interval in milliseconds (default: 10ms)
Returns:
start: () => void- Start the stopwatchstop: () => void- Stop the stopwatchreset: () => void- Reset to zerotoggle: () => void- Toggle between start and stoplap: () => LapTime- Record a lap timeclearLaps: () => void- Clear all lap timesisRunning: boolean- Current running stateelapsedTime: number- Elapsed time in millisecondslapTimes: LapTime[]- Array of recorded lap timesformattedTime: string- Formatted time (HH:MM:SS.mmm)
useRetry
Retry failed async operations with exponential backoff and customizable retry logic.
import { useRetry } from 'use-time-hooks';
import { useState } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const fetchData = async (id: string) => {
const response = await fetch(`/api/data/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
};
const { execute, cancel, reset, state } = useRetry(fetchData, {
maxAttempts: 5,
initialDelay: 1000,
backoffMultiplier: 2,
maxDelay: 30000,
shouldRetry: (error, attempt) => {
// Only retry on network errors or 5xx status codes
return error.message.includes('Failed to fetch');
},
onRetry: (error, attempt, delay) => {
console.log(`Retry attempt ${attempt} in ${delay}ms`);
},
});
const handleFetch = async () => {
try {
setError(null);
const result = await execute('123');
setData(result);
} catch (err) {
setError(err);
}
};
return (
<div>
<button onClick={handleFetch} disabled={state.isRetrying}>
{state.isRetrying ? 'Retrying...' : 'Fetch Data'}
</button>
<button onClick={cancel} disabled={!state.isRetrying}>
Cancel
</button>
<button onClick={reset}>Reset</button>
{state.isRetrying && (
<p>
Attempt {state.currentAttempt + 1} of {state.totalAttempts}
{state.timeUntilNextRetry > 0 && (
<span>
{' '}
- Next retry in: {(state.timeUntilNextRetry / 1000).toFixed(1)}s
</span>
)}
</p>
)}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{error && <p>Error: {error.message}</p>}
</div>
);
}Parameters:
operation: (...args) => Promise<T>- Async operation to retryoptions?: RetryOptions- Configuration optionsmaxAttempts?: number- Maximum retry attempts (default: 3)initialDelay?: number- Initial delay in ms (default: 1000)backoffMultiplier?: number- Exponential backoff multiplier (default: 2)maxDelay?: number- Maximum delay in ms (default: 30000)useExponentialBackoff?: boolean- Use exponential backoff (default: true)shouldRetry?: (error, attemptNumber) => boolean- Custom retry logiconRetry?: (error, attemptNumber, nextDelay) => void- Retry callbackonMaxAttemptsReached?: (lastError, totalAttempts) => void- Max attempts callback
Returns:
execute: (...args) => Promise<T>- Execute with retry logiccancel: () => void- Cancel ongoing retryreset: () => void- Reset retry statestate: RetryState- Current retry state
useBatchedUpdates
Batch multiple updates over time to reduce re-renders and improve performance.
import { useBatchedUpdates } from 'use-time-hooks';
import { useState } from 'react';
function BatchedUpdatesDemo() {
const [messages, setMessages] = useState<string[]>([]);
const {
addUpdate,
flush,
clear,
pendingUpdates,
batchSize,
hasPendingUpdates,
timeUntilFlush,
} = useBatchedUpdates(
(batchedUpdates: string[]) => {
// This will be called with all batched updates at once
setMessages((prev) => [...prev, ...batchedUpdates]);
},
{
batchWindow: 500, // Wait 500ms before flushing
maxBatchSize: 10, // Or flush when 10 updates accumulated
onFlush: (updates, size) => {
console.log(`Flushed ${size} updates`);
},
}
);
const addMessage = () => {
addUpdate(`Message ${Date.now()}`);
};
return (
<div>
<button onClick={addMessage}>Add Message</button>
<button onClick={flush} disabled={!hasPendingUpdates}>
Flush Now ({batchSize})
</button>
<button onClick={clear} disabled={!hasPendingUpdates}>
Clear Pending
</button>
<p>Pending: {batchSize}</p>
{hasPendingUpdates && (
<p>Auto-flush in: {(timeUntilFlush / 1000).toFixed(1)}s</p>
)}
<div>
{messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))}
</div>
</div>
);
}Parameters:
onBatchFlush: (batchedUpdates: T[]) => void- Callback when batch is flushedoptions?: BatchedUpdatesOptions- Configuration optionsbatchWindow?: number- Time window in ms (default: 100)maxBatchSize?: number- Max batch size (default: 50)flushOnFirst?: boolean- Flush immediately on first update (default: false)reducer?: (accumulated, newUpdate) => any[]- Custom reduceronFlush?: (batchedUpdates, batchSize) => void- Flush callback
Returns:
addUpdate: (update: T) => void- Add update to batchflush: () => void- Manually flush pending updatesclear: () => void- Clear pending updatespendingUpdates: T[]- Current batchbatchSize: number- Number of pending updateshasPendingUpdates: boolean- Whether there are pending updatestimeUntilFlush: number- Time until auto-flush in ms
useDelayedState
State with delayed updates, useful for optimistic UI updates or debouncing state changes.
import { useDelayedState } from 'use-time-hooks';
import { useState } from 'react';
function DelayedStateDemo() {
const {
value,
immediateValue,
setValue,
setImmediate,
cancel,
isPending,
timeRemaining,
} = useDelayedState('', 2000);
const [input, setInput] = useState('');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInput(newValue);
setValue(newValue); // Will update 'value' after 2 seconds
};
return (
<div>
<input
value={input}
onChange={handleInputChange}
placeholder="Type something..."
/>
<div>
<p>Immediate: {immediateValue}</p>
<p>Delayed: {value}</p>
{isPending && <p>Update in: {(timeRemaining / 1000).toFixed(1)}s</p>}
</div>
<button onClick={() => setImmediate(input)}>Apply Immediately</button>
<button onClick={cancel} disabled={!isPending}>
Cancel Pending
</button>
</div>
);
}Parameters:
initialValue: T- Initial state valuedelay: number- Delay in millisecondsimmediate?: boolean- First update immediate (default: false)
Returns:
value: T- Current value (may be delayed)immediateValue: T- Immediate value (not delayed)setValue: (newValue) => void- Set value with delaysetImmediate: (newValue) => void- Set value immediatelycancel: () => void- Cancel pending updateisPending: boolean- Whether update is pendingtimeRemaining: number- Time until update in ms
useSequentialExecution
Execute an array of functions sequentially with specified delays between each execution.
import { useSequentialExecution, ExecutionStep } from 'use-time-hooks';
import { useState } from 'react';
function SequentialDemo() {
const [messages, setMessages] = useState<string[]>([]);
const steps: ExecutionStep[] = [
{
fn: () => setMessages((prev) => [...prev, 'Step 1 executed']),
timeout: 1000,
id: 'step1',
},
{
fn: () => setMessages((prev) => [...prev, 'Step 2 executed']),
timeout: 2000,
id: 'step2',
},
{
fn: () => setMessages((prev) => [...prev, 'Step 3 executed']),
timeout: 1500,
id: 'step3',
},
];
const {
start,
stop,
reset,
toggle,
isRunning,
currentStepIndex,
cyclesCompleted,
timeRemaining,
currentStep,
} = useSequentialExecution(steps, true, false); // loop=true, autoStart=false
return (
<div>
<button onClick={toggle}>{isRunning ? 'Stop' : 'Start'}</button>
<button onClick={reset}>Reset</button>
<p>Status: {isRunning ? 'Running' : 'Stopped'}</p>
<p>
Step: {currentStepIndex + 1}/{steps.length}
</p>
<p>Cycles: {cyclesCompleted}</p>
<p>Next in: {(timeRemaining / 1000).toFixed(1)}s</p>
<p>Current Step: {currentStep?.id || 'None'}</p>
<div>
{messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))}
</div>
</div>
);
}Parameters:
steps: ExecutionStep[]- Array of steps to executefn: () => void | Promise<void>- Function to executetimeout: number- Timeout before executing in msid?: string- Optional identifier
loop?: boolean- Loop back to beginning (default: true)autoStart?: boolean- Start immediately (default: false)
Returns:
start: () => void- Start executionstop: () => void- Stop executionreset: () => void- Reset to beginningtoggle: () => void- Toggle between start and stopisRunning: boolean- Current running statecurrentStepIndex: number- Current step indexcyclesCompleted: number- Number of cycles completedtimeRemaining: number- Time remaining until next executioncurrentStep: ExecutionStep | null- Current step being executed
TypeScript Support
This library is written in TypeScript and provides full type definitions out of the box. All hooks are fully typed with proper generic support where applicable.
Generic Hooks
Several hooks support TypeScript generics for type-safe usage:
useDebounce<T> - Preserves the type of the debounced value:
const [searchTerm, setSearchTerm] = useState<string>('');
const debouncedSearch = useDebounce<string>(searchTerm, 500);
// debouncedSearch is typed as stringuseRetry<T> - Types the return value of the operation:
interface UserData {
id: string;
name: string;
}
const fetchUser = async (): Promise<UserData> => {
const response = await fetch('/api/user');
return response.json();
};
const { execute, state } = useRetry<UserData>(fetchUser, {
maxAttempts: 3
});
// execute() returns Promise<UserData>
const userData = await execute();useBatchedUpdates<T> - Types the batched update items:
interface LogEntry {
timestamp: number;
message: string;
}
const { addUpdate } = useBatchedUpdates<LogEntry>(
(entries) => {
console.log('Batch:', entries);
}
);
// addUpdate expects LogEntry type
addUpdate({ timestamp: Date.now(), message: 'Hello' });useDelayedState<T> - Types the state value:
interface FormData {
email: string;
password: string;
}
const { value, setValue, isPending } = useDelayedState<FormData>(
{ email: '', password: '' },
2000
);
// value is typed as FormData
console.log(value.email);Type Exports
All hooks export their return type interfaces for advanced usage:
import type {
UseIntervalReturn,
UseTimeoutReturn,
UseStopwatchReturn,
UseRetryReturn,
UseBatchedUpdatesReturn,
UseDelayedStateReturn,
UseSequentialExecutionReturn,
LapTime,
ExecutionStep,
RetryState
} from 'use-time-hooks';Quick Start
import { useInterval, useDebounce, useTimeout } from 'use-time-hooks';
import { useState } from 'react';
function App() {
// Simple counter with interval
const [count, setCount] = useState(0);
const { toggle, isRunning } = useInterval(() => {
setCount((c) => c + 1);
}, 1000);
// Debounced search
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
// Delayed notification
useTimeout(() => {
console.log('5 seconds passed!');
}, 5000);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={toggle}>{isRunning ? 'Pause' : 'Start'}</button>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<p>Debounced: {debouncedSearch}</p>
</div>
);
}Examples
For complete, runnable examples of each hook, check out the examples/ folder:
Basic Hooks
- useInterval (JS) - Counter and timer examples
- useTimeout (JS) - Delayed notifications with controls
- useDebounce (JS) - Search input and form validation
- useThrottle (JS) - Scroll tracking and resize handling
- useStopwatch (JS) - Stopwatch with lap timing
Advanced Hooks
- useRetry (JS) - Retry failed operations with exponential backoff
- useBatchedUpdates (JS) - Batch multiple updates for performance
- useDelayedState (JS) - State with delayed updates and optimistic UI
- useSequentialExecution (JS) - Execute functions sequentially with control
Each example file contains multiple use cases demonstrating different features of the hooks. Examples are available in both TypeScript and JavaScript versions.
Live Demo
Check out the interactive demo site to see all hooks in action:
cd demos
npm install
npm run devVisit http://localhost:5173 to explore the demos.
Browser Compatibility
This library works with React 16.8+ (hooks support required) and supports all modern browsers.
License
MIT Š SkorpionG
Repository
https://github.com/SkorpionG/use-time-hooks
Author
Created with â¤ī¸ by SkorpionG