Skip to content

Commit bbf9422

Browse files
committed
fix(useCopyToClipboard): cancel reset timer on unmount and re-click
Stores the timeout id in a ref so unmount (e.g. modal close right after copy) cancels the pending setCopied(false), avoiding a stale setState on a dead component tree. The same ref re-arms on rapid re-click so the previous timer can't flip the flag back mid- feedback.
1 parent 9063710 commit bbf9422

1 file changed

Lines changed: 24 additions & 3 deletions

File tree

‎src/lib/useCopyToClipboard.ts‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import { useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22

33
/** Copy-to-clipboard with a transient "copied" flag for button UI.
44
* `getPayload` is called at copy-time (not at hook-call-time) so
55
* live-generated content reflects the current state — needed for
66
* the Setup-Script live-clock mode where the emitted ^ST stamp
7-
* must be "now at click", not "now at last render". */
7+
* must be "now at click", not "now at last render".
8+
*
9+
* Holds the reset timer in a ref so two cases are handled cleanly:
10+
* (1) a second copy click while the previous reset is still pending
11+
* re-arms instead of letting the older timer flip the flag back
12+
* mid-feedback; (2) the host component unmounting (e.g. modal
13+
* close immediately after copy) cancels the timer in the effect
14+
* cleanup so React doesn't run a stale setState on a dead tree. */
815
export function useCopyToClipboard(getPayload: () => string, resetMs = 1500) {
916
const [copied, setCopied] = useState(false);
17+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1018

1119
const copy = () => {
1220
const payload = getPayload();
@@ -17,9 +25,22 @@ export function useCopyToClipboard(getPayload: () => string, resetMs = 1500) {
1725
if (!navigator.clipboard) return;
1826
navigator.clipboard.writeText(payload).then(() => {
1927
setCopied(true);
20-
setTimeout(() => setCopied(false), resetMs);
28+
if (timerRef.current !== null) clearTimeout(timerRef.current);
29+
timerRef.current = setTimeout(() => {
30+
timerRef.current = null;
31+
setCopied(false);
32+
}, resetMs);
2133
}).catch(() => { /* swallow: user-cancel or permission-denied */ });
2234
};
2335

36+
useEffect(() => {
37+
return () => {
38+
if (timerRef.current !== null) {
39+
clearTimeout(timerRef.current);
40+
timerRef.current = null;
41+
}
42+
};
43+
}, []);
44+
2445
return { copy, copied };
2546
}

0 commit comments

Comments
 (0)