From 1dfcfaa6c3e6033e65786c52ede74db822d48dbb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 27 Jun 2026 08:28:05 +0000 Subject: [PATCH] Prevent unnecessary re-rendering of table rows --- .../components/forms/fields/TableField.tsx | 42 ++++++++++++++----- src/frontend/src/forms/StockForms.tsx | 15 ++++++- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index 08b520b95b..3e41a9f5c1 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -15,20 +15,12 @@ import { StandaloneField } from '../StandaloneField'; export interface TableFieldRowProps { item: any; idx: number; - rowId?: string | number; + rowId: string | number; rowErrors: any; changeFn: (idx: number | string, key: string, value: any) => void; removeFn: (idx: number | string) => void; } -function getRowIdentifier(item: any, idx: number): string | number { - if (item && typeof item === 'object') { - return item.pk ?? item.item ?? item.id ?? item.uuid ?? idx; - } - - return item ?? idx; -} - function TableFieldRow({ item, idx, @@ -40,7 +32,7 @@ function TableFieldRow({ }: Readonly<{ item: any; idx: number; - rowId?: string | number; + rowId: string | number; errors: any; definition: ApiFormFieldType; changeFn: (idx: number, key: string, value: any) => void; @@ -110,6 +102,34 @@ export function TableField({ const valueRef = useRef(value); const onChangeRef = useRef(field.onChange); const rowIndexByIdRef = useRef(new Map()); + const generatedRowIdsRef = useRef(new WeakMap()); + const generatedRowIdCounterRef = useRef(0); + + const getRowIdentifier = useCallback( + (item: any, idx: number): string | number => { + if (item && typeof item === 'object') { + const intrinsicId = item.pk ?? item.item ?? item.id ?? item.uuid; + + if (intrinsicId !== undefined && intrinsicId !== null) { + return intrinsicId; + } + + const existingGeneratedId = generatedRowIdsRef.current.get(item); + + if (existingGeneratedId) { + return existingGeneratedId; + } + + generatedRowIdCounterRef.current += 1; + const generatedId = `table-row-generated-${generatedRowIdCounterRef.current}`; + generatedRowIdsRef.current.set(item, generatedId); + return generatedId; + } + + return item ?? idx; + }, + [] + ); // Keep refs in sync with latest values without introducing them as deps valueRef.current = value; @@ -123,7 +143,7 @@ export function TableField({ }); rowIndexByIdRef.current = nextRowIndexById; - }, [value]); + }, [value, getRowIdentifier]); const resolveRowIndex = useCallback((identifier: number | string) => { const mappedIndex = rowIndexByIdRef.current.get(identifier); diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index d293385238..08196f2737 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -634,7 +634,7 @@ function StockOperationsRow({ returnStock?: boolean; record?: any; }) { - const rowId = props.rowId ?? props.idx; + const rowId = props.rowId; useWhyDidYouUpdate(`StockOperationsRow-${rowId}`, props); @@ -847,9 +847,20 @@ function StockOperationsRow({ const MemoizedStockOperationsRow = memo( StockOperationsRow, (previousProps, nextProps) => { + const prevItem = previousProps.props.item; + const nextItem = nextProps.props.item; + + const itemUnchanged = + prevItem === nextItem || + (prevItem?.pk === nextItem?.pk && + prevItem?.quantity === nextItem?.quantity && + prevItem?.status === nextItem?.status && + prevItem?.packaging === nextItem?.packaging && + prevItem?.merge === nextItem?.merge); + return ( previousProps.props.rowId === nextProps.props.rowId && - previousProps.props.item === nextProps.props.item && + itemUnchanged && previousProps.props.rowErrors === nextProps.props.rowErrors && previousProps.props.changeFn === nextProps.props.changeFn && previousProps.props.removeFn === nextProps.props.removeFn &&