From 4ec67056e516e340c8ee01f7632db7b9872ded15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 17:08:26 +0200 Subject: [PATCH] refactor(health-score): hoist COMPONENT_ORDER, memoise asOf formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit COMPONENT_ORDER moves to module scope so the provenance row order is allocated once per process. The asOf date formatter is memoised on the locale via useMemo so the per-row map no longer constructs a fresh Intl.DateTimeFormat instance for each row. The defensive try/catch around new Date() drops at the same time — the constructor doesn't throw on bad ISO input, the Number.isNaN(getTime()) guard already catches every failure mode. --- src/components/insights/health-score-card.tsx | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/insights/health-score-card.tsx b/src/components/insights/health-score-card.tsx index 905265af1..92c71ee7a 100644 --- a/src/components/insights/health-score-card.tsx +++ b/src/components/insights/health-score-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useId, useState } from "react"; +import { useId, useMemo, useState } from "react"; import { ArrowDown, ArrowUp, ChevronDown, Minus, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; @@ -134,6 +134,16 @@ const COMPONENT_LABEL_KEY: Record< compliance: "insights.healthScore.componentCompliance", }; +// v1.4.25 W8e — provenance row order. Module-scope so the array is +// allocated once per process rather than on every card render; the +// downstream `.map()` is non-mutating, so a fresh copy isn't needed. +const COMPONENT_ORDER: readonly ComponentKey[] = [ + "bp", + "weight", + "mood", + "compliance", +]; + export function HealthScoreCard({ score, band, @@ -147,19 +157,22 @@ export function HealthScoreCard({ // (`title`) so the row layout stays one-line. Dates format via the // user's locale to honour the EU comma / dot convention; on bad input // we fall through to the raw ISO so the user still sees something. - const formatAsOf = (asOf: string | null): string | null => { - if (!asOf) return null; - try { - const parsed = new Date(asOf); - if (Number.isNaN(parsed.getTime())) return null; - return new Intl.DateTimeFormat(locale, { + // The formatter is memoised per locale so the row map below doesn't + // build a fresh `Intl.DateTimeFormat` instance per row. + const asOfFormatter = useMemo( + () => + new Intl.DateTimeFormat(locale, { year: "numeric", month: "short", day: "2-digit", - }).format(parsed); - } catch { - return null; - } + }), + [locale], + ); + const formatAsOf = (asOf: string | null): string | null => { + if (!asOf) return null; + const parsed = new Date(asOf); + if (Number.isNaN(parsed.getTime())) return null; + return asOfFormatter.format(parsed); }; const [expanded, setExpanded] = useState(initiallyExpanded); // `useId` keeps the aria-controls/section-id pair unique even when @@ -179,13 +192,7 @@ export function HealthScoreCard({ // descending so the biggest contributor sits first. Components with // null values sink to the bottom (`weight * 0 === 0`); the tie-break // is the alphabetical key order so determinism holds across renders. - const COMPONENT_ORDER: readonly ComponentKey[] = [ - "bp", - "weight", - "mood", - "compliance", - ]; - const provenanceRows = [...COMPONENT_ORDER] + const provenanceRows = COMPONENT_ORDER .map((key) => { const c = components[key]; const inferredSource: HealthScoreComponentSource =