Enhance ObjectSelect Component with Improved State Management and Value Handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced refs for loaded keys and tree select value to optimize state management and prevent unnecessary re-renders.
- Refactored getValueIdentity function to handle various data types more effectively, including null and boolean values.
- Updated selection handling logic to ensure accurate comparisons and prevent redundant updates during value changes.
- Enhanced loading logic to track loaded nodes and improve the rendering of child components based on selection state.
- Implemented additional useEffect hooks to synchronize state changes with component updates, enhancing overall responsiveness.
This commit is contained in:
Tom Butcher 2026-09-15 01:56:57 +01:00
parent da2881d505
commit 968dc0667a

View File

@ -251,6 +251,8 @@ const ObjectSelect = ({
const masterFilterRef = useRef(masterFilter) const masterFilterRef = useRef(masterFilter)
const clearedMissingValueRef = useRef(false) const clearedMissingValueRef = useRef(false)
const loadGenerationRef = useRef(0) const loadGenerationRef = useRef(0)
const loadedKeysRef = useRef(new Set())
const treeSelectValueRef = useRef(null)
const getSelectKey = useCallback( const getSelectKey = useCallback(
(selectType, selectMasterFilter) => (selectType, selectMasterFilter) =>
`${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`, `${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`,
@ -269,31 +271,25 @@ const ObjectSelect = ({
// Normalize a value to an identity string so we can detect in-place _id updates // Normalize a value to an identity string so we can detect in-place _id updates
const getValueIdentity = useCallback((val) => { const getValueIdentity = useCallback((val) => {
if (val && typeof val === 'object') { if (val == null || val === '') return ''
// Handle arrays if (
if (Array.isArray(val)) { typeof val === 'string' ||
const ids = val typeof val === 'number' ||
.map((item) => { typeof val === 'boolean'
if (item && typeof item === 'object') { ) {
if (item._id) return String(item._id) return String(val).toLowerCase()
if (
item.value &&
typeof item.value === 'object' &&
item.value._id
)
return String(item.value._id)
}
return null
})
.filter(Boolean)
.sort()
return ids.length > 0 ? ids.join(',') : JSON.stringify(val)
}
// Handle single objects
if (val._id) return String(val._id)
if (val.value && typeof val.value === 'object' && val.value._id)
return String(val.value._id)
} }
if (typeof val !== 'object') return String(val)
if (Array.isArray(val)) {
const ids = val
.map((item) => getValueIdentity(item))
.filter(Boolean)
.sort()
return ids.join(',')
}
if (val._id) return String(val._id).toLowerCase()
if (val.value != null) return getValueIdentity(val.value)
return JSON.stringify(val) return JSON.stringify(val)
}, []) }, [])
const prevValueIdentityRef = useRef(getValueIdentity(value)) const prevValueIdentityRef = useRef(getValueIdentity(value))
@ -426,11 +422,7 @@ const ObjectSelect = ({
masterFilterRef.current masterFilterRef.current
) )
) { ) {
if (itemExists) { silentReloadRef.current?.()
reloadRef.current?.()
} else {
silentReloadRef.current?.()
}
return return
} }
@ -445,7 +437,7 @@ const ObjectSelect = ({
}, []) }, [])
const newEventHandler = useCallback(() => { const newEventHandler = useCallback(() => {
reloadRef.current?.() silentReloadRef.current?.()
}, []) }, [])
const reloadRef = useRef(null) const reloadRef = useRef(null)
@ -536,8 +528,8 @@ const ObjectSelect = ({
newFilterPath, newFilterPath,
objects objects
) )
const resolvedChildren = const loaded =
nodeChildren.length === 0 ? undefined : nodeChildren nodeChildren.length > 0 || loadedKeysRef.current.has(nodeKey)
const modelProperty = getModelProperty(type, property) const modelProperty = getModelProperty(type, property)
return { return {
title: <ObjectProperty {...modelProperty} value={value} />, title: <ObjectProperty {...modelProperty} value={value} />,
@ -549,7 +541,8 @@ const ObjectSelect = ({
filterPath: newFilterPath, filterPath: newFilterPath,
selectable: false, selectable: false,
isLeaf: false, isLeaf: false,
children: resolvedChildren loaded,
children: loaded ? nodeChildren : undefined
} }
}) })
.filter(Boolean) .filter(Boolean)
@ -608,6 +601,7 @@ const ObjectSelect = ({
async (node) => { async (node) => {
if (!node.property) return if (!node.property) return
if (type == 'unknown') return if (type == 'unknown') return
if (node.key) loadedKeysRef.current.add(node.key)
await handleFetchObjectsProperties(buildFilterFromNode(node)) await handleFetchObjectsProperties(buildFilterFromNode(node))
}, },
[buildFilterFromNode, handleFetchObjectsProperties, type] [buildFilterFromNode, handleFetchObjectsProperties, type]
@ -696,6 +690,13 @@ const ObjectSelect = ({
const onTreeSelectChange = useCallback( const onTreeSelectChange = useCallback(
(nextValue) => { (nextValue) => {
if (
getValueIdentity(nextValue) ===
getValueIdentity(treeSelectValueRef.current)
) {
return
}
const isEmptySelection = multiple const isEmptySelection = multiple
? !Array.isArray(nextValue) || nextValue.length === 0 ? !Array.isArray(nextValue) || nextValue.length === 0
: nextValue == null || nextValue === '' : nextValue == null || nextValue === ''
@ -744,7 +745,7 @@ const ObjectSelect = ({
} }
onChange?.(null) onChange?.(null)
}, },
[multiple, onChange, findObjectById] [multiple, onChange, findObjectById, getValueIdentity]
) )
const onSearch = useCallback( const onSearch = useCallback(
@ -856,6 +857,10 @@ const ObjectSelect = ({
treeDataRef.current = treeData treeDataRef.current = treeData
}, [treeData]) }, [treeData])
useEffect(() => {
treeSelectValueRef.current = treeSelectValue
}, [treeSelectValue])
const prevValuesRef = useRef({ type, masterFilter }) const prevValuesRef = useRef({ type, masterFilter })
useEffect(() => { useEffect(() => {
@ -877,6 +882,7 @@ const ObjectSelect = ({
treeDataRef.current = [] treeDataRef.current = []
setTreeVersion((v) => v + 1) setTreeVersion((v) => v + 1)
setExpandedKeys([]) setExpandedKeys([])
loadedKeysRef.current = new Set()
setInitialized(false) setInitialized(false)
valueRef.current = null valueRef.current = null
setTreeSelectValue(null) setTreeSelectValue(null)
@ -899,13 +905,16 @@ const ObjectSelect = ({
const changeSource = isInternalChangeRef.current ? 'internal' : 'external' const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
if (changeSource == 'external') { if (changeSource == 'external') {
loadGenerationRef.current += 1 const nextId = getValueId(value)
setObjectPropertiesTree({}) const alreadyInTree = isValueInTree(treeDataRef.current, nextId)
setTreeData([]) const alreadySelected =
treeDataRef.current = [] treeSelectValueRef.current != null &&
setInitialized(false) getValueIdentity(treeSelectValueRef.current) === currentValueIdentity
setInitialLoading(true)
valueRef.current = null if (!alreadyInTree && !alreadySelected) {
loadGenerationRef.current += 1
}
clearedMissingValueRef.current = false clearedMissingValueRef.current = false
setValueNotFound(false) setValueNotFound(false)
} }
@ -924,6 +933,40 @@ const ObjectSelect = ({
const generation = loadGenerationRef.current const generation = loadGenerationRef.current
const handleValue = async () => { const handleValue = async () => {
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
const valueIdentity = getValueIdentity(value)
const ids = multiple
? (Array.isArray(value) ? value.map(getValueId) : [])
: value == null
? []
: [getValueId(value)]
const allInTree =
ids.length > 0 &&
ids.every(
(id) =>
id == null ||
id === '' ||
isValueInTree(treeDataRef.current, id) ||
findObjectById(id) != null
)
if (
value != null &&
type != 'unknown' &&
allInTree &&
getValueIdentity(valueRef.current) !== valueIdentity
) {
valueRef.current = value
setTreeSelectValue(
multiple
? ids.map((id) => toSelectValue(id)).filter((id) => id != null)
: toSelectValue(ids[0])
)
setInitialized(true)
setInitialLoading(false)
return
}
if ( if (
multiple && multiple &&
Array.isArray(value) && Array.isArray(value) &&
@ -975,6 +1018,7 @@ const ObjectSelect = ({
} }
setExpandedKeys([...new Set(pathKeys)]) setExpandedKeys([...new Set(pathKeys)])
pathKeys.forEach((key) => loadedKeysRef.current.add(key))
setTreeSelectValue( setTreeSelectValue(
value value
.map((item) => toSelectValue(getValueId(item))) .map((item) => toSelectValue(getValueId(item)))
@ -1025,6 +1069,7 @@ const ObjectSelect = ({
} }
}) })
setExpandedKeys(pathKeys) setExpandedKeys(pathKeys)
pathKeys.forEach((key) => loadedKeysRef.current.add(key))
const data = await handleFetchObjectsProperties(valueFilter) const data = await handleFetchObjectsProperties(valueFilter)
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data) if (Array.isArray(data)) applyTreeFromData(data)
@ -1069,7 +1114,8 @@ const ObjectSelect = ({
connected, connected,
getValueIdentity, getValueIdentity,
multiple, multiple,
applyTreeFromData applyTreeFromData,
findObjectById
]) ])
useEffect(() => { useEffect(() => {