From b34d78b9a823093b2d9f75ae13747004ee2ebe9b Mon Sep 17 00:00:00 2001 From: LockedTerminal Date: Sun, 6 Sep 2026 03:44:43 +0000 Subject: [PATCH] perf: memoize derived values and stabilize callbacks in Landing and RegistryTable - Landing: move STEPS to module-level const; wrap intensity computation and verify-rows array in useMemo so they are only recomputed when pool data or the translation function changes - RegistryTable: wrap toggleSort, handleEdit, handleCancel, handleSave in useCallback; hoist all translated labels into a single useMemo block; wrap Th and Row sub-components in React.memo so sort-triggered parent re-renders do not cascade to unchanged rows/headers - Row now accepts onEdit(id) / onSave(id, credit, green) signatures so the parent can pass stable handlers without per-row closures; open() and handleSave inside Row are stabilised with useCallback Closes #388 --- src/screens/Landing.tsx | 33 ++++-- src/screens/admin/RegistryTable.tsx | 158 ++++++++++++++++++---------- 2 files changed, 125 insertions(+), 66 deletions(-) diff --git a/src/screens/Landing.tsx b/src/screens/Landing.tsx index ac4eedf..d1b1a54 100644 --- a/src/screens/Landing.tsx +++ b/src/screens/Landing.tsx @@ -1,5 +1,6 @@ 'use client' +import { useMemo } from 'react' import { useTranslations } from 'next-intl' import { Button, StatBlock } from '../components' import { LiveHelio } from '../brand/LiveHelio' @@ -14,11 +15,30 @@ export interface LandingProps { onExplore: () => void } +// Stable reference — never changes at runtime. +const STEPS = [1, 2, 3, 4] as const + export function Landing({ onConnect, onExplore }: LandingProps) { const t = useTranslations('Landing') const d = HB_DATA - const steps = [1, 2, 3, 4] as const - const intensity = Math.min(1, d.pool.totalAssets / 6_000_000) + + // Derived numeric value — recompute only when pool data changes. + const intensity = useMemo( + () => Math.min(1, d.pool.totalAssets / 6_000_000), + [d.pool.totalAssets], + ) + + // Verify-rows array — rebuilt only when translation function changes. + const verifyRows = useMemo( + () => + [ + [t('rowRegistry'), 'C…7K4Z'], + [t('rowVault'), 'C…9QWJ'], + [t('rowCadence'), t('rowCadenceValue')], + [t('rowAudit'), t('rowAuditValue')], + ] as const, + [t], + ) return (
@@ -145,7 +165,7 @@ export function Landing({ onConnect, onExplore }: LandingProps) { {t('howSub')}

- {steps.map((i) => ( + {STEPS.map((i) => (
- {[ - [t('rowRegistry'), 'C…7K4Z'], - [t('rowVault'), 'C…9QWJ'], - [t('rowCadence'), t('rowCadenceValue')], - [t('rowAudit'), t('rowAuditValue')], - ].map(([k, v]) => ( + {verifyRows.map(([k, v]) => (
{ - if (key === sortKey) { - setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) - } else { - setSortKey(key) - setSortDir(key === 'name' || key === 'type' || key === 'lastVerified' ? 'asc' : 'desc') - } - } + // Stable sort toggler — only recreated when sort state changes. + const toggleSort = useCallback( + (key: SortKey) => { + if (key === sortKey) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) + } else { + setSortKey(key) + setSortDir(key === 'name' || key === 'type' || key === 'lastVerified' ? 'asc' : 'desc') + } + }, + [sortKey], + ) + + // Stable row-level action handlers — Row receives these plus its own id + // and calls them with the id; avoids one new closure per row per render. + const handleEdit = useCallback((id: number) => setEditing(id), []) + const handleCancel = useCallback(() => setEditing(null), []) + const handleSave = useCallback( + (id: number, credit: number, green: number) => { + onSave(id, credit, green) + setEditing(null) + }, + [onSave], + ) + + // Translated labels — stable as long as locale doesn't change. + const labels = useMemo( + () => ({ + colProject: t('colProject'), + colType: t('colType'), + colCredit: t('colCredit'), + colGreen: t('colGreen'), + colFunded: t('colFunded'), + colLastVerified: t('colLastVerified'), + colActions: t('colActions'), + sortByProject: t('sortBy', { col: t('colProject') }), + sortByType: t('sortBy', { col: t('colType') }), + sortByCredit: t('sortBy', { col: t('colCredit') }), + sortByGreen: t('sortBy', { col: t('colGreen') }), + sortByFunded: t('sortBy', { col: t('colFunded') }), + sortByLastVerified: t('sortBy', { col: t('colLastVerified') }), + updateScores: t('updateScores'), + actionCancel: t('actionCancel'), + actionSave: t('actionSave'), + scoreFieldCredit: t('scoreFieldCredit'), + scoreFieldGreen: t('scoreFieldGreen'), + }), + [t], + ) return (
@@ -63,59 +104,59 @@ export function RegistryTable({ rows, onSave }: RegistryTableProps) { - {t('colActions')} + {labels.colActions} @@ -125,18 +166,15 @@ export function RegistryTable({ rows, onSave }: RegistryTableProps) { key={r.id} row={r} editing={editing === r.id} - onEdit={() => setEditing(r.id)} - onCancel={() => setEditing(null)} - onSave={(credit, green) => { - onSave(r.id, credit, green) - setEditing(null) - }} - updateLabel={t('updateScores')} + onEdit={handleEdit} + onCancel={handleCancel} + onSave={handleSave} + updateLabel={labels.updateScores} reVerifyLabel={t('reVerify', { name: r.name })} - creditFieldLabel={t('scoreFieldCredit')} - greenFieldLabel={t('scoreFieldGreen')} - cancelLabel={t('actionCancel')} - saveLabel={t('actionSave')} + creditFieldLabel={labels.scoreFieldCredit} + greenFieldLabel={labels.scoreFieldGreen} + cancelLabel={labels.actionCancel} + saveLabel={labels.actionSave} /> ))} @@ -145,7 +183,8 @@ export function RegistryTable({ rows, onSave }: RegistryTableProps) { ) } -function Th({ +// Memoised — only re-renders when its own sort props change. +const Th = memo(function Th({ label, k, sortKey, @@ -199,9 +238,25 @@ function Th({ ) +}) + +// Callbacks accept the row id so the parent can use stable, non-closure handlers. +interface RowProps { + row: RegistryEntry + editing: boolean + onEdit: (id: number) => void + onCancel: () => void + onSave: (id: number, credit: number, green: number) => void + updateLabel: string + reVerifyLabel: string + creditFieldLabel: string + greenFieldLabel: string + cancelLabel: string + saveLabel: string } -function Row({ +// Memoised — skips re-render unless its own row data or editing state changes. +const Row = memo(function Row({ row, editing, onEdit, @@ -213,28 +268,21 @@ function Row({ greenFieldLabel, cancelLabel, saveLabel, -}: { - row: RegistryEntry - editing: boolean - onEdit: () => void - onCancel: () => void - onSave: (credit: number, green: number) => void - updateLabel: string - reVerifyLabel: string - creditFieldLabel: string - greenFieldLabel: string - cancelLabel: string - saveLabel: string -}) { +}: RowProps) { const [credit, setCredit] = useState(String(row.credit)) const [green, setGreen] = useState(String(row.green)) // Reset draft to current values each time the editor opens. - const open = () => { + const open = useCallback(() => { setCredit(String(row.credit)) setGreen(String(row.green)) - onEdit() - } + onEdit(row.id) + }, [row.credit, row.green, row.id, onEdit]) + + const handleSave = useCallback( + () => onSave(row.id, clampScore(credit), clampScore(green)), + [row.id, credit, green, onSave], + ) return ( <> @@ -280,11 +328,7 @@ function Row({ -
@@ -294,7 +338,7 @@ function Row({ )} ) -} +}) function ScoreField({ label,