diff --git a/client/src/components/ui/CopyButton.tsx b/client/src/components/ui/CopyButton.tsx index 653a8d49..05a9ff39 100644 --- a/client/src/components/ui/CopyButton.tsx +++ b/client/src/components/ui/CopyButton.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Copy, Check } from 'lucide-react'; import toast from 'react-hot-toast'; @@ -11,6 +11,11 @@ interface CopyButtonProps { export function CopyButton({ text, label = 'Copied!', size = 14, className = '' }: CopyButtonProps) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef>(); + + useEffect(() => { + return () => clearTimeout(timeoutRef.current); + }, []); const handleCopy = async () => { if (!navigator?.clipboard?.writeText) { @@ -21,7 +26,8 @@ export function CopyButton({ text, label = 'Copied!', size = 14, className = '' if (success) { setCopied(true); toast.success(label); - setTimeout(() => setCopied(false), 2000); + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 2000); } else { toast.error('Failed to copy to clipboard'); } diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index 70e212e6..572957ce 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -27,16 +27,24 @@ export function useSSE( // Proxy every named event through the ref so the EventSource instance // stays stable while handler implementations can change freely. const names = Object.keys(handlersRef.current); + const listeners: Array<[string, (e: Event) => void]> = []; for (const name of names) { if (name === 'message') { eventSource.onmessage = (e) => handlersRef.current.message?.(e); } else { - eventSource.addEventListener(name, (e) => { + const listener = (e: Event) => { handlersRef.current[name]?.(e as MessageEvent); - }); + }; + eventSource.addEventListener(name, listener); + listeners.push([name, listener]); } } - return () => eventSource.close(); + return () => { + for (const [name, listener] of listeners) { + eventSource.removeEventListener(name, listener); + } + eventSource.close(); + }; }, [url]); }