From 36831bd97166be48502f4547898a812f78b6fbc8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Jun 2026 00:23:02 +0000 Subject: [PATCH] Generic row memos --- .../components/forms/fields/TableField.tsx | 70 ++++++++++++++++--- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index 7f3c6439eb..810b028b14 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -30,23 +30,25 @@ export interface TableFieldRowProps { function TableFieldRow({ item, rowId, - errors, - definition, + rowErrors, + modelRenderer, + columnCount, changeFn, removeFn }: Readonly<{ item: any; rowId: string | number; - errors: any; - definition: ApiFormFieldType; + rowErrors: any; + modelRenderer?: ApiFormFieldType['modelRenderer']; + columnCount?: number; changeFn: (idx: number, key: string, value: any) => void; removeFn: (idx: number | string) => void; }>) { // Table fields require render function - if (!definition.modelRenderer) { + if (!modelRenderer) { return ( - + }> {t`modelRenderer entry required for tables`} @@ -55,15 +57,60 @@ function TableFieldRow({ ); } - return definition.modelRenderer({ + return modelRenderer({ item: item, rowId: rowId, - rowErrors: errors, + rowErrors: rowErrors, changeFn: changeFn, removeFn: removeFn }); } +// Memoize each table field row, so that we don't re-render the entire table when a single row is updated +function areRowItemsEqual(previousItem: any, nextItem: any): boolean { + if (previousItem === nextItem) { + return true; + } + + if (!previousItem || !nextItem) { + return false; + } + + if (typeof previousItem !== 'object' || typeof nextItem !== 'object') { + return previousItem === nextItem; + } + + const previousKeys = Object.keys(previousItem); + const nextKeys = Object.keys(nextItem); + + if (previousKeys.length !== nextKeys.length) { + return false; + } + + for (const key of previousKeys) { + if (previousItem[key] !== nextItem[key]) { + return false; + } + } + + return true; +} + +const MemoizedTableFieldRow = memo( + TableFieldRow, + (previousProps, nextProps) => { + return ( + previousProps.rowId === nextProps.rowId && + areRowItemsEqual(previousProps.item, nextProps.item) && + previousProps.rowErrors === nextProps.rowErrors && + previousProps.modelRenderer === nextProps.modelRenderer && + previousProps.changeFn === nextProps.changeFn && + previousProps.removeFn === nextProps.removeFn && + previousProps.columnCount === nextProps.columnCount + ); + } +); + export function TableFieldErrorWrapper({ props, errorKey, @@ -257,12 +304,13 @@ function TableFieldComponent({ const rowId = getRowIdentifier(item, idx); return ( -