diff --git a/components/dashboard/ExportDataDialog.tsx b/components/dashboard/ExportDataDialog.tsx index 11a4e4d..b036b94 100644 --- a/components/dashboard/ExportDataDialog.tsx +++ b/components/dashboard/ExportDataDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { Download, FileText, Braces, CheckCircle2, Layers } from "lucide-react"; import { Dialog, @@ -69,9 +69,19 @@ export function ExportDataDialog({ }: ExportDataDialogProps): React.JSX.Element { const [selectedFormat, setSelectedFormat] = useState("csv"); const [isExporting, setIsExporting] = useState(false); + const timeoutRef = useRef | null>(null); const isLargeExport = events.length >= STREAM_THRESHOLD; + // Cleanup timeout on unmount to prevent memory leaks + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + async function handleExport(): Promise { setIsExporting(true); @@ -107,9 +117,15 @@ export function ExportDataDialog({ } } } finally { - setTimeout(function () { + // Clear any existing timeout to prevent memory leaks + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(function () { setIsExporting(false); onOpenChange(false); + timeoutRef.current = null; }, 400); } } diff --git a/components/dashboard/RawDataDialog.tsx b/components/dashboard/RawDataDialog.tsx index 547e1c9..0f6f42c 100644 --- a/components/dashboard/RawDataDialog.tsx +++ b/components/dashboard/RawDataDialog.tsx @@ -1,7 +1,7 @@ "use client"; import { Code, ExternalLink, Copy, Check } from "lucide-react"; -import React, { useState } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Dialog, DialogContent, @@ -31,14 +31,33 @@ interface RawDataDialogProps { function CopyButton({ text }: { text: string }): React.JSX.Element { const [copied, setCopied] = useState(false); const { toast } = useToast(); + const timeoutRef = useRef | null>(null); async function handleCopy(): Promise { await navigator.clipboard.writeText(text); setCopied(true); toast({ description: "Copied!" }); - setTimeout(() => setCopied(false), 2000); + + // Clear any existing timeout to prevent memory leaks + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + setCopied(false); + timeoutRef.current = null; + }, 2000); } + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + return ( diff --git a/components/dashboard/SecurityMetricsDashboard.tsx b/components/dashboard/SecurityMetricsDashboard.tsx index 392e134..9ab0a9e 100644 --- a/components/dashboard/SecurityMetricsDashboard.tsx +++ b/components/dashboard/SecurityMetricsDashboard.tsx @@ -47,11 +47,11 @@ export function SecurityMetricsDashboard() { const fetchMetrics = async () => { try { const response = await fetch("/api/security/metrics"); - + if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - + const data = await response.json(); setMetrics(data); setError(null); @@ -64,10 +64,11 @@ export function SecurityMetricsDashboard() { useEffect(() => { fetchMetrics(); - + // Refresh every 10 seconds const interval = setInterval(fetchMetrics, 10000); - + + // Cleanup interval on unmount to prevent memory leaks return () => clearInterval(interval); }, []); diff --git a/lib/hooks/useLiveFeed.ts b/lib/hooks/useLiveFeed.ts index 916da13..f390500 100644 --- a/lib/hooks/useLiveFeed.ts +++ b/lib/hooks/useLiveFeed.ts @@ -58,6 +58,7 @@ export function useLiveFeed(onEvent: (event: TranslatedEvent) => void): LiveFeed const pauseBufferRef = useRef([]); const isPausedRef = useRef(false); const onEventRef = useRef(onEvent); + const timeoutIdsRef = useRef>>(new Set()); onEventRef.current = onEvent; // Tracks the current reconnection attempt count. Reset to 0 on a successful @@ -136,7 +137,9 @@ export function useLiveFeed(onEvent: (event: TranslatedEvent) => void): LiveFeed next.delete(event.raw.id); return next; }); + timeoutIdsRef.current.delete(timeoutId); }, 600); + timeoutIdsRef.current.add(timeoutId); }; ws.onclose = () => { @@ -197,13 +200,15 @@ export function useLiveFeed(onEvent: (event: TranslatedEvent) => void): LiveFeed for (const event of buffered) { onEventRef.current(event); setNewEventIds((ids) => new Set(ids).add(event.raw.id)); - setTimeout(() => { + const timeoutId = setTimeout(() => { setNewEventIds((ids) => { const next = new Set(ids); next.delete(event.raw.id); return next; }); + timeoutIdsRef.current.delete(timeoutId); }, 600); + timeoutIdsRef.current.add(timeoutId); } }