Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 76x 41x 76x 76x 76x 76x 45x 45x 76x 93x 27x 27x 76x 12x 12x 76x 44x 44x 44x 44x 17x 17x 17x 76x 8x 8x 1x 7x 76x 43x 6x 6x 37x 37x 37x 76x | import { useCallback, useEffect, useRef, useState } from "react";
/**
* Delay type for useTimeout
* - number: delay in milliseconds
* - null | undefined: disable timer
*/
export type TimeoutDelay = number | null | undefined;
/**
* Callback type for useTimeout
*/
export type UseTimeoutCallback = () => void;
/**
* Return type for useTimeout hook
*/
export interface UseTimeoutReturn {
/**
* Reset timer (restart from beginning)
*/
reset: () => void;
/**
* Cancel timer (callback won't execute)
*/
clear: () => void;
/**
* Whether timer is pending
*/
isPending: boolean;
}
/**
* A hook for declarative setTimeout with automatic cleanup and controls.
*
* Provides a safe way to use setTimeout in React components with:
* - Automatic cleanup on unmount (prevents memory leaks)
* - Auto-reset when delay changes
* - Disable timer when delay is null/undefined
* - Always maintains latest callback reference (prevents stale closure)
* - isPending state to check timer status
*
* @param callback - Function to execute after delay
* @param delay - Delay in milliseconds, or null/undefined to disable
* @returns Object containing reset, clear functions and isPending state
*
* @example
* ```tsx
* // Auto-dismissing toast
* function Toast({ message }: { message: string }) {
* const [show, setShow] = useState(true);
*
* useTimeout(() => {
* setShow(false);
* }, 3000);
*
* return show ? <div>{message}</div> : null;
* }
* ```
*
* @example
* ```tsx
* // Debounced auto-save with reset
* function Editor() {
* const [content, setContent] = useState("");
*
* const { reset } = useTimeout(() => {
* saveToServer(content);
* }, 2000);
*
* const handleChange = (value: string) => {
* setContent(value);
* reset(); // Reset timer on every keystroke
* };
*
* return <textarea onChange={(e) => handleChange(e.target.value)} />;
* }
* ```
*
* @example
* ```tsx
* // Conditional execution
* function SessionTimeout({ isLoggedIn }: { isLoggedIn: boolean }) {
* useTimeout(
* () => {
* logout();
* alert("Session expired");
* },
* isLoggedIn ? 30 * 60 * 1000 : null // Only when logged in
* );
*
* return <div>...</div>;
* }
* ```
*/
export function useTimeout(
callback: UseTimeoutCallback,
delay: TimeoutDelay
): UseTimeoutReturn {
// Lazily initialize so an active numeric delay reports `isPending` on the
// first committed render — avoids an extra mount render and a one-frame-stale
// value, and keeps the client's initial value consistent with the server.
const [isPending, setIsPending] = useState<boolean>(
() => delay !== null && delay !== undefined
);
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const callbackRef = useRef<UseTimeoutCallback>(callback);
const delayRef = useRef<TimeoutDelay>(delay);
// Always keep the latest callback and delay references so that `reset` can
// read the current delay without depending on it (keeps `reset` stable).
useEffect(() => {
callbackRef.current = callback;
delayRef.current = delay;
}, [callback, delay]);
// Clear timeout helper
const clearTimeoutHelper = useCallback(() => {
if (timeoutIdRef.current !== null) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
}, []);
// Clear function (exposed)
const clear = useCallback(() => {
clearTimeoutHelper();
setIsPending(false);
}, [clearTimeoutHelper]);
// Set timeout helper
const setTimeoutHelper = useCallback(
(delayMs: number) => {
// Clear existing timeout
clearTimeoutHelper();
// Normalize delay: treat negative as 0
const normalizedDelay = Math.max(0, delayMs);
setIsPending(true);
timeoutIdRef.current = setTimeout(() => {
timeoutIdRef.current = null;
setIsPending(false);
callbackRef.current();
}, normalizedDelay);
},
[clearTimeoutHelper]
);
// Reset function (exposed). Reads the delay through a ref so its identity
// stays stable even when `delay` changes.
const reset = useCallback(() => {
const currentDelay = delayRef.current;
if (currentDelay === null || currentDelay === undefined) {
return;
}
setTimeoutHelper(currentDelay);
}, [setTimeoutHelper]);
// Effect: setup/cleanup timeout when delay changes
useEffect(() => {
// If delay is null or undefined, clear and don't set new timer
if (delay === null || delay === undefined) {
clear();
return;
}
// Set new timeout
setTimeoutHelper(delay);
// Cleanup on unmount or delay change
return () => {
clearTimeoutHelper();
};
}, [delay, setTimeoutHelper, clearTimeoutHelper, clear]);
return {
reset,
clear,
isPending,
};
}
|