From ec89d64dbf3a85f0f90144a8df2f7c065047ee7d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sat, 1 Aug 2026 10:21:23 +0700 Subject: [PATCH] fix(fe): crash 'reading channel_id' + recharts width/height warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMessageDetail: guard attachments fetcher — revalidate bisa race detail load, detail.data undefined saat fetcher jalan -> TypeError 'Cannot read properties of undefined (reading channel_id)' yang bikin halaman /messages mati (Next error boundary). Sekarang fetcher balikin [] kalau channel_id belum ada; hapus non-null assertion. Diverifikasi: /messages sebelumnya crash, sekarang render dengan data asli (list, verdict, confidence, sticker, emoji). - ResponsiveContainer (recharts 3.8): initialDimension -1 di render pertama -> warning 'width(-1) and height(-1)'. Pakai height numerik tetap (192/160/48) + minWidth/minHeight 0 -> calculatedHeight >0, warning hilang; width tetap responsif via ResizeObserver. Chart baru dirender setelah mount (useMounted) biar container punya ukuran. Diverifikasi console: 0 warning, 0 error di dashboard & voice. --- .../src/components/dashboard/stat-card.tsx | 11 ++- .../dashboard/top-channels-chart.tsx | 76 ++++++++++------- .../components/voice/activity-timeline.tsx | 83 +++++++++++-------- services/frontend/src/hooks/use-messages.ts | 13 +-- .../frontend/src/lib/hooks/use-mounted.ts | 16 ++++ 5 files changed, 126 insertions(+), 73 deletions(-) create mode 100644 services/frontend/src/lib/hooks/use-mounted.ts diff --git a/services/frontend/src/components/dashboard/stat-card.tsx b/services/frontend/src/components/dashboard/stat-card.tsx index e18f68e..37de095 100644 --- a/services/frontend/src/components/dashboard/stat-card.tsx +++ b/services/frontend/src/components/dashboard/stat-card.tsx @@ -3,6 +3,7 @@ import type { LucideIcon } from "lucide-react"; import { Area, AreaChart, ResponsiveContainer } from "recharts"; import { GlassCard } from "@/components/glass/card"; +import { useMounted } from "@/lib/hooks/use-mounted"; import { cn } from "@/lib/utils"; interface StatCardProps { @@ -22,6 +23,7 @@ export function StatCard({ sparklineData, formatter = (v) => (typeof v === "number" ? v.toLocaleString() : v), }: StatCardProps) { + const mounted = useMounted(); const accentColor = { default: "var(--color-primary)", danger: "var(--color-destructive)", @@ -54,9 +56,14 @@ export function StatCard({ {/* Sparkline background */} - {sparklineData && sparklineData.length > 0 && ( + {sparklineData && sparklineData.length > 0 && mounted && (
- +
@@ -23,38 +26,47 @@ export function TopChannelsChart({ data = [] }: TopChannelsChartProps) {
- - - - - - - - + {mounted ? ( + + + + + + + + + ) : ( +
+ )}
); diff --git a/services/frontend/src/components/voice/activity-timeline.tsx b/services/frontend/src/components/voice/activity-timeline.tsx index c658657..7ff2691 100644 --- a/services/frontend/src/components/voice/activity-timeline.tsx +++ b/services/frontend/src/components/voice/activity-timeline.tsx @@ -1,6 +1,5 @@ "use client"; -import { GlassCard } from "@/components/glass/card"; import { Bar, BarChart, @@ -9,12 +8,16 @@ import { XAxis, YAxis, } from "recharts"; +import { GlassCard } from "@/components/glass/card"; +import { useMounted } from "@/lib/hooks/use-mounted"; interface ActivityTimelineProps { data?: { user: string; duration: number }[]; } export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) { + const mounted = useMounted(); + return (
@@ -23,39 +26,51 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
- - - - - [`${(Number(value) / 60).toFixed(1)}m`, "Duration"]} - /> - - - + {mounted ? ( + + + + + [ + `${(Number(value) / 60).toFixed(1)}m`, + "Duration", + ]} + /> + + + + ) : ( +
+ )}
); diff --git a/services/frontend/src/hooks/use-messages.ts b/services/frontend/src/hooks/use-messages.ts index 4d22794..ecdbe89 100644 --- a/services/frontend/src/hooks/use-messages.ts +++ b/services/frontend/src/hooks/use-messages.ts @@ -126,14 +126,17 @@ export function useReview(channelId?: string) { export function useMessageDetail(id: string | null) { const detail = useSWR(id ? msgKeys.detail(id) : null, () => - messagesApi.getDetail(id!), + messagesApi.getDetail(id ?? ""), ); + const channelId = id ? detail.data?.channel_id : undefined; const attachments = useSWR( - id && detail.data?.channel_id - ? [...msgKeys.detail(id), "attachments"] - : null, + channelId ? [...msgKeys.detail(id ?? ""), "attachments"] : null, async () => { - const res = await messagesApi.getAttachments(detail.data!.channel_id, 10); + // Guard: only fetch when we actually have a channel id — a revalidate + // can race the detail load and see detail.data === undefined. + const cid = detail.data?.channel_id; + if (!cid) return []; + const res = await messagesApi.getAttachments(cid, 10); return res.data; }, ); diff --git a/services/frontend/src/lib/hooks/use-mounted.ts b/services/frontend/src/lib/hooks/use-mounted.ts new file mode 100644 index 0000000..80d4373 --- /dev/null +++ b/services/frontend/src/lib/hooks/use-mounted.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +/** + * Returns true once the component has mounted on the client. Charts wrapped + * in ResponsiveContainer measure their parent during the first layout — if + * the container has no size yet (hydration/flex/grid), Recharts logs + * "width(-1) and height(-1)" warnings. Deferring the chart render until after + * mount guarantees the container has real dimensions. + */ +export function useMounted(): boolean { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + return mounted; +}