From 479fdd91da8a9f371738be5ae2325538770f6822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sun, 14 Sep 2025 15:05:06 +0200 Subject: [PATCH 1/3] Start working on optimized items measurement --- .../shared/DraggableView/ActiveItemPortal.tsx | 11 +- .../shared/DraggableView/DraggableView.tsx | 31 +-- .../shared/DraggableView/ItemCell.tsx | 82 +++--- .../DraggableView/TeleportedItemCell.tsx | 7 +- .../shared/ItemsProvider/ItemOutlet.tsx | 13 +- .../providers/shared/MeasurementsProvider.ts | 240 ++++++++---------- 6 files changed, 161 insertions(+), 223 deletions(-) diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx index 53e8677c..9d1ebc27 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import type { ManualGesture } from 'react-native-gesture-handler'; import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; @@ -42,7 +42,7 @@ export default function ActiveItemPortal({ const teleportEnabled = useMutableValue(false); const isFirstUpdateRef = useRef(true); - const renderTeleportedItemCell = useCallback( + const teleportedItemCell = useMemo( () => ( // We have to wrap the TeleportedItemCell in context providers as they won't // be accessible otherwise, when the item is rendered in the portal outlet @@ -77,7 +77,7 @@ export default function ActiveItemPortal({ const enableTeleport = useStableCallback(() => { isFirstUpdateRef.current = true; - teleport?.(teleportedItemId, renderTeleportedItemCell()); + teleport?.(teleportedItemId, teleportedItemCell); onTeleport(true); }); @@ -93,8 +93,7 @@ export default function ActiveItemPortal({ if (!checkTeleported()) return; const update = () => - checkTeleported() && - teleport?.(teleportedItemId, renderTeleportedItemCell()); + checkTeleported() && teleport?.(teleportedItemId, teleportedItemCell); if (isFirstUpdateRef.current) { isFirstUpdateRef.current = false; @@ -103,7 +102,7 @@ export default function ActiveItemPortal({ } else { update(); } - }, [isTeleported, renderTeleportedItemCell, teleport, teleportedItemId]); + }, [isTeleported, teleportedItemCell, teleport, teleportedItemId]); useAnimatedReaction( () => activationAnimationProgress.value, diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx index b2c38523..178f1fcf 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx @@ -1,9 +1,8 @@ -import { Fragment, memo, useCallback, useEffect, useState } from 'react'; -import type { LayoutChangeEvent } from 'react-native'; +import { Fragment, memo, useRef, useState } from 'react'; import { GestureDetector } from 'react-native-gesture-handler'; +import type Animated from 'react-native-reanimated'; import { LayoutAnimationConfig, - runOnUI, useDerivedValue } from 'react-native-reanimated'; @@ -16,10 +15,8 @@ import { ItemContextProvider, ItemOutlet, useCommonValuesContext, - useDragContext, useItemLayout, useItemPanGesture, - useMeasurementsContext, usePortalContext } from '../../../providers'; import ActiveItemPortal from './ActiveItemPortal'; @@ -40,11 +37,9 @@ function DraggableView({ }: DraggableViewProps) { const portalContext = usePortalContext(); const commonValuesContext = useCommonValuesContext(); - const { handleItemMeasurement, removeItemMeasurements } = - useMeasurementsContext(); - const { handleDragEnd } = useDragContext(); const { activeItemKey, customHandle } = commonValuesContext; + const cellRef = useRef(null); const [isHidden, setIsHidden] = useState(false); const activationAnimationProgress = useMutableValue(0); const isActive = useDerivedValue(() => activeItemKey.value === key); @@ -55,24 +50,6 @@ function DraggableView({ ); const gesture = useItemPanGesture(key, activationAnimationProgress); - useEffect(() => { - return () => { - removeItemMeasurements(key); - runOnUI(() => { - handleDragEnd(key, activationAnimationProgress); - })(); - }; - }, [activationAnimationProgress, handleDragEnd, key, removeItemMeasurements]); - - const onLayout = useCallback( - ({ - nativeEvent: { - layout: { height, width } - } - }: LayoutChangeEvent) => handleItemMeasurement(key, { height, width }), - [handleItemMeasurement, key] - ); - const renderItemCell = (hidden = false) => { const innerComponent = ( + ref={cellRef}> diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx index 5fbf6b1d..f789b4a2 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx @@ -1,10 +1,5 @@ import type { PropsWithChildren } from 'react'; -import { - type LayoutChangeEvent, - Platform, - StyleSheet, - type ViewStyle -} from 'react-native'; +import { Platform, StyleSheet, type ViewStyle } from 'react-native'; import type { SharedValue, TransformArrayItem } from 'react-native-reanimated'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; @@ -14,7 +9,7 @@ import type { LayoutAnimation } from '../../../integrations/reanimated'; import { useCommonValuesContext, useItemDecoration } from '../../../providers'; -import AnimatedOnLayoutView from '../AnimatedOnLayoutView'; +import { componentWithRef } from '../../../utils/react'; type TransformsArray = Array; @@ -27,52 +22,55 @@ export type ItemCellProps = PropsWithChildren<{ hidden?: boolean; entering?: LayoutAnimation; exiting?: LayoutAnimation; - onLayout?: (event: LayoutChangeEvent) => void; }>; -export default function ItemCell({ - activationAnimationProgress, - baseStyle, - children, - entering, - exiting, - hidden, - isActive, - itemKey, - layoutStyleValue, - onLayout -}: ItemCellProps) { - const { controlledItemDimensionsStyle } = useCommonValuesContext(); +const ItemCell = componentWithRef( + function ItemCell( + { + activationAnimationProgress, + baseStyle, + children, + entering, + exiting, + hidden, + isActive, + itemKey, + layoutStyleValue + }, + ref + ) { + const { controlledItemDimensionsStyle } = useCommonValuesContext(); - const decorationStyleValue = useItemDecoration( - itemKey, - isActive, - activationAnimationProgress - ); + const decorationStyleValue = useItemDecoration( + itemKey, + isActive, + activationAnimationProgress + ); - const animatedStyle = useAnimatedStyle(() => { - return { + const animatedStyle = useAnimatedStyle(() => ({ ...decorationStyleValue.value, ...layoutStyleValue.value, transform: [ ...((layoutStyleValue.value.transform ?? []) as TransformsArray), ...((decorationStyleValue.value.transform ?? []) as TransformsArray) ] - }; - }); + })); - return ( - - - - ); -} + return ( + + + + ); + } +); + +export default ItemCell; const styles = StyleSheet.create({ decoration: Platform.select({ diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/TeleportedItemCell.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/TeleportedItemCell.tsx index baaa9e61..2ed75abd 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/TeleportedItemCell.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/TeleportedItemCell.tsx @@ -11,7 +11,6 @@ type TeleportedItemCellProps = Pick< | 'children' | 'isActive' | 'itemKey' - | 'onLayout' >; export default function TeleportedItemCell({ @@ -19,8 +18,7 @@ export default function TeleportedItemCell({ baseStyle, children, isActive, - itemKey, - onLayout + itemKey }: TeleportedItemCellProps) { const teleportedItemLayoutValue = useTeleportedItemLayout( itemKey, @@ -34,8 +32,7 @@ export default function TeleportedItemCell({ baseStyle={baseStyle} isActive={isActive} itemKey={itemKey} - layoutStyleValue={teleportedItemLayoutValue} - onLayout={onLayout}> + layoutStyleValue={teleportedItemLayoutValue}> {children} ); diff --git a/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemOutlet.tsx b/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemOutlet.tsx index a323070a..cb64a02e 100644 --- a/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemOutlet.tsx +++ b/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemOutlet.tsx @@ -1,13 +1,20 @@ -import { memo } from 'react'; +import { memo, useLayoutEffect } from 'react'; import { useItemNode } from './hooks'; type ItemOutletProps = { itemKey: string; + onUpdate?: () => void; }; -function ItemOutlet({ itemKey }: ItemOutletProps) { - return useItemNode(itemKey); +function ItemOutlet({ itemKey, onUpdate }: ItemOutletProps) { + const node = useItemNode(itemKey); + + useLayoutEffect(() => { + onUpdate?.(); + }, [node, onUpdate]); + + return node; } export default memo(ItemOutlet); diff --git a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts index 522502c0..50e1eb85 100644 --- a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts @@ -1,20 +1,13 @@ -import { useCallback, useRef } from 'react'; -import type { SharedValue } from 'react-native-reanimated'; +import { useCallback } from 'react'; +import type { View } from 'react-native'; +import type { AnimatedRef } from 'react-native-reanimated'; import { runOnUI } from 'react-native-reanimated'; -import { useStableCallback } from '../../hooks'; import { setAnimatedTimeout, - useAnimatedDebounce, useMutableValue } from '../../integrations/reanimated'; -import type { - Dimension, - Dimensions, - ItemSizes, - MeasurementsContextType -} from '../../types'; -import { areValuesDifferent, resolveDimension } from '../../utils'; +import type { Dimensions, MeasurementsContextType } from '../../types'; import { createProvider } from '../utils'; import { useCommonValuesContext } from './CommonValuesProvider'; import { useItemsContext } from './ItemsProvider'; @@ -50,132 +43,102 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( const { getKeys } = useItemsContext(); const context = useMutableValue(null); - const previousItemDimensionsRef = useRef>({}); - const debounce = useAnimatedDebounce(); - - const handleItemMeasurement = useStableCallback( - (key: string, dimensions: Dimensions) => { - const prevDimensions = previousItemDimensionsRef.current[key]; - - const { height: isHeightControlled, width: isWidthControlled } = - controlledItemDimensions; - if (isWidthControlled && isHeightControlled) { - return; - } - - const changedDimensions: Partial = {}; - - if ( - !isWidthControlled && - areValuesDifferent(prevDimensions?.width, dimensions.width, 1) - ) { - changedDimensions.width = dimensions.width; - } - if ( - !isHeightControlled && - areValuesDifferent(prevDimensions?.height, dimensions.height, 1) - ) { - changedDimensions.height = dimensions.height; - } - - if (!Object.keys(changedDimensions).length) { - return; - } - - previousItemDimensionsRef.current[key] = dimensions; - const itemsCount = getKeys().length; - - runOnUI(() => { - context.value ??= { - measuredItemKeys: new Set(), - queuedMeasurements: new Map() - }; - - const ctx = context.value; - - const isNewItem = - !ctx.measuredItemKeys.has(key) && - (resolveDimension(itemWidths.value, key) === null || - resolveDimension(itemHeights.value, key) === null); - - if (isNewItem) { - ctx.measuredItemKeys.add(key); - } - - ctx.queuedMeasurements.set(key, dimensions); - - if (activeItemKey.value === key) { - activeItemDimensions.value = dimensions; - if (multiZoneActiveItemDimensions) { - multiZoneActiveItemDimensions.value = dimensions; - } - } - - // Update the array of item dimensions only after all items have been - // measured to reduce the number of times animated reactions are triggered - if (ctx.measuredItemKeys.size !== itemsCount) { - return; - } - - const updateDimensions = () => { - const updateDimension = ( - dimension: Dimension, - sizes: SharedValue - ) => { - const newSizes = { ...(sizes.value as Record) }; - for (const [k, dims] of ctx.queuedMeasurements.entries()) { - newSizes[k] = dims[dimension]; - } - sizes.value = newSizes; - }; - - if (!isWidthControlled) { - updateDimension('width', itemWidths); - } - if (!isHeightControlled) { - updateDimension('height', itemHeights); - } - - ctx.queuedMeasurements.clear(); - debounce.cancel(); - }; - - if (isNewItem || ctx.queuedMeasurements.size === itemsCount) { - // Update dimensions immediately to avoid unnecessary delays when: - // - measurements were triggered because of adding new items and all new items have been measured - // - all sortable container items' dimensions have changed (e.g. when someone creates collapsible - // items which change their height when the user starts dragging them) - updateDimensions(); - } else { - // In all other cases, debounce the update to reduce the number of - // updates when dimensions change many times within a short period of time - debounce.schedule(updateDimensions, measureDebounceDelay); - } - })(); - } - ); - - const removeItemMeasurements = useCallback( - (key: string) => { - delete previousItemDimensionsRef.current[key]; - const { height: isHeightControlled, width: isWidthControlled } = - controlledItemDimensions; - if (isWidthControlled && isHeightControlled) { - return; - } - - runOnUI(() => { - if (itemWidths.value && typeof itemWidths.value === 'object') { - delete itemWidths.value[key]; - } - if (itemHeights.value && typeof itemHeights.value === 'object') { - delete itemHeights.value[key]; - } - context.value?.measuredItemKeys.delete(key); - })(); - }, - [controlledItemDimensions, itemHeights, itemWidths, context] - ); + const itemRefs = useMutableValue>>({}); + + // const handleItemMeasurement = useStableCallback( + // (key: string, dimensions: Dimensions) => { + // 'worklet'; + // runOnUI(() => { + // context.value ??= { + // measuredItemKeys: new Set(), + // queuedMeasurements: new Map() + // }; + + // const ctx = context.value; + + // const isNewItem = + // !ctx.measuredItemKeys.has(key) && + // (resolveDimension(itemWidths.value, key) === null || + // resolveDimension(itemHeights.value, key) === null); + + // if (isNewItem) { + // ctx.measuredItemKeys.add(key); + // } + + // ctx.queuedMeasurements.set(key, dimensions); + + // if (activeItemKey.value === key) { + // activeItemDimensions.value = dimensions; + // if (multiZoneActiveItemDimensions) { + // multiZoneActiveItemDimensions.value = dimensions; + // } + // } + + // // Update the array of item dimensions only after all items have been + // // measured to reduce the number of times animated reactions are triggered + // if (ctx.measuredItemKeys.size !== itemsCount) { + // return; + // } + + // const updateDimensions = () => { + // const updateDimension = ( + // dimension: Dimension, + // sizes: SharedValue + // ) => { + // const newSizes = { ...(sizes.value as Record) }; + // for (const [k, dims] of ctx.queuedMeasurements.entries()) { + // newSizes[k] = dims[dimension]; + // } + // sizes.value = newSizes; + // }; + + // if (!isWidthControlled) { + // updateDimension('width', itemWidths); + // } + // if (!isHeightControlled) { + // updateDimension('height', itemHeights); + // } + + // ctx.queuedMeasurements.clear(); + // debounce.cancel(); + // }; + + // if (isNewItem || ctx.queuedMeasurements.size === itemsCount) { + // // Update dimensions immediately to avoid unnecessary delays when: + // // - measurements were triggered because of adding new items and all new items have been measured + // // - all sortable container items' dimensions have changed (e.g. when someone creates collapsible + // // items which change their height when the user starts dragging them) + // updateDimensions(); + // } else { + // // In all other cases, debounce the update to reduce the number of + // // updates when dimensions change many times within a short period of time + // debounce.schedule(updateDimensions, measureDebounceDelay); + // } + // })(); + // } + // ); + + // const removeItemMeasurements = useCallback( + // (key: string) => { + // delete previousItemDimensionsRef.current[key]; + // const { height: isHeightControlled, width: isWidthControlled } = + // controlledItemDimensions; + // if (isWidthControlled && isHeightControlled) { + // return; + // } + + // runOnUI(() => { + // if (itemWidths.value && typeof itemWidths.value === 'object') { + // delete itemWidths.value[key]; + // } + // if (itemHeights.value && typeof itemHeights.value === 'object') { + // delete itemHeights.value[key]; + // } + // context.value?.measuredItemKeys.delete(key); + // })(); + // }, + // [controlledItemDimensions, itemHeights, itemWidths, context] + // ); const handleContainerMeasurement = useCallback( (width: number, height: number) => { @@ -223,7 +186,6 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( ); const resetMeasurements = useCallback(() => { - previousItemDimensionsRef.current = {}; runOnUI(() => { context.value = null; if (typeof itemWidths.value === 'object') { @@ -239,8 +201,6 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( value: { applyControlledContainerDimensions, handleContainerMeasurement, - handleItemMeasurement, - removeItemMeasurements, resetMeasurements } }; From 92f65afb928f5582e22007d8d8998c3dd99f5a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sun, 14 Sep 2025 16:34:50 +0200 Subject: [PATCH 2/3] Huge simplification of the measurements provider --- .../src/components/SortableGrid.tsx | 15 +- .../shared/DraggableView/DraggableView.tsx | 11 +- .../src/constants/props.ts | 1 - .../shared/ItemsProvider/ItemsProvider.tsx | 1 + .../providers/shared/ItemsProvider/store.ts | 40 ++- .../providers/shared/MeasurementsProvider.ts | 239 ++++++++---------- .../src/providers/shared/SharedProvider.tsx | 4 +- .../src/types/props/shared.ts | 5 - .../src/types/providers/shared.ts | 11 +- .../src/utils/equality.ts | 14 +- 10 files changed, 175 insertions(+), 166 deletions(-) diff --git a/packages/react-native-sortables/src/components/SortableGrid.tsx b/packages/react-native-sortables/src/components/SortableGrid.tsx index b8a494cb..989ddc81 100644 --- a/packages/react-native-sortables/src/components/SortableGrid.tsx +++ b/packages/react-native-sortables/src/components/SortableGrid.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useMemo, useRef } from 'react'; +import { useMemo } from 'react'; import type { DimensionValue } from 'react-native'; import { StyleSheet } from 'react-native'; import type { SharedValue } from 'react-native-reanimated'; @@ -193,22 +193,11 @@ function SortableGridComponent({ strategy, ...rest }: SortableGridComponentProps) { - const { handleContainerMeasurement, resetMeasurements } = - useMeasurementsContext(); + const { handleContainerMeasurement } = useMeasurementsContext(); const { mainGroupSize } = useGridLayoutContext(); - const isFirstRenderRef = useRef(true); - useOrderUpdater(strategy, GRID_STRATEGIES); - useLayoutEffect(() => { - if (isFirstRenderRef.current) { - isFirstRenderRef.current = false; - return; - } - resetMeasurements(); - }, [groups, resetMeasurements]); - const animatedInnerStyle = useAnimatedStyle(() => isVertical ? { diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx index 178f1fcf..710382dd 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx @@ -1,8 +1,9 @@ -import { Fragment, memo, useRef, useState } from 'react'; +import { Fragment, memo, useEffect, useState } from 'react'; import { GestureDetector } from 'react-native-gesture-handler'; import type Animated from 'react-native-reanimated'; import { LayoutAnimationConfig, + useAnimatedRef, useDerivedValue } from 'react-native-reanimated'; @@ -17,6 +18,7 @@ import { useCommonValuesContext, useItemLayout, useItemPanGesture, + useMeasurementsContext, usePortalContext } from '../../../providers'; import ActiveItemPortal from './ActiveItemPortal'; @@ -37,9 +39,10 @@ function DraggableView({ }: DraggableViewProps) { const portalContext = usePortalContext(); const commonValuesContext = useCommonValuesContext(); + const { registerItem } = useMeasurementsContext(); const { activeItemKey, customHandle } = commonValuesContext; - const cellRef = useRef(null); + const itemRef = useAnimatedRef(); const [isHidden, setIsHidden] = useState(false); const activationAnimationProgress = useMutableValue(0); const isActive = useDerivedValue(() => activeItemKey.value === key); @@ -50,6 +53,8 @@ function DraggableView({ ); const gesture = useItemPanGesture(key, activationAnimationProgress); + useEffect(() => registerItem(key, itemRef), [key, registerItem, itemRef]); + const renderItemCell = (hidden = false) => { const innerComponent = ( + ref={itemRef}> diff --git a/packages/react-native-sortables/src/constants/props.ts b/packages/react-native-sortables/src/constants/props.ts index a4d9abe1..2dbba818 100644 --- a/packages/react-native-sortables/src/constants/props.ts +++ b/packages/react-native-sortables/src/constants/props.ts @@ -50,7 +50,6 @@ export const DEFAULT_SHARED_PROPS = { itemEntering: IS_WEB ? null : SortableItemEntering, itemExiting: IS_WEB ? null : SortableItemExiting, itemsLayoutTransitionMode: 'all', - measureDebounceDelay: 0, onActiveItemDropped: undefined, onDragMove: undefined, onDragStart: undefined, diff --git a/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemsProvider.tsx b/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemsProvider.tsx index 2d4008ec..10e70689 100644 --- a/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemsProvider.tsx +++ b/packages/react-native-sortables/src/providers/shared/ItemsProvider/ItemsProvider.tsx @@ -29,6 +29,7 @@ const { ItemsProvider, useItemsContext } = createProvider('Items')< getKeys: store.getKeys, getNode: store.getNode, subscribeItem: store.subscribeItem, + subscribeItems: store.subscribeItems, subscribeKeys: store.subscribeKeys } }; diff --git a/packages/react-native-sortables/src/providers/shared/ItemsProvider/store.ts b/packages/react-native-sortables/src/providers/shared/ItemsProvider/store.ts index f405e549..8ee7feb9 100644 --- a/packages/react-native-sortables/src/providers/shared/ItemsProvider/store.ts +++ b/packages/react-native-sortables/src/providers/shared/ItemsProvider/store.ts @@ -8,6 +8,9 @@ export type Store = { getNode: (key: string) => ReactNode | undefined; subscribeItem: (key: string, listener: () => void) => () => void; + subscribeItems: ( + listener: (updatedItemKeys: Array) => void + ) => () => void; update: ( entries: Array<[key: string, item: I]>, @@ -28,6 +31,7 @@ export function createItemsStore( const keyListeners = new Set<() => void>(); const itemListeners = new Map void>>(); + const itemsListeners = new Set<(updatedItemKeys: Array) => void>(); // Track the renderer to detect changes let currentRenderer: RenderItem | undefined = initialRenderItem; @@ -50,6 +54,19 @@ export function createItemsStore( }; }; + const subscribeItems = ( + listener: (updatedItemKeys: Array) => void + ) => { + itemsListeners.add(listener); + + // Notify immediately with current items if any exist + if (keys.length > 0) { + queueMicrotask(() => listener([...keys])); + } + + return () => itemsListeners.delete(listener); + }; + // Core logic for init + updates function apply( entries: Array<[string, I]>, @@ -74,7 +91,7 @@ export function createItemsStore( keys = nextKeys; } - const touched = new Set(); + const updatedItemKeys = new Set(); entries.forEach(([k, item], index) => { const prev = meta.get(k); @@ -86,16 +103,24 @@ export function createItemsStore( const info = { index, item }; meta.set(k, info); nodes.set(k, renderItem ? renderItem(info) : (item as ReactNode)); - touched.add(k); + updatedItemKeys.add(k); }); if (!notify) return; if (keysChanged) keyListeners.forEach(fn => fn()); - touched.forEach(k => { + updatedItemKeys.forEach(k => { const subs = itemListeners.get(k); if (subs) subs.forEach(fn => fn()); }); + + // Notify items listeners if any items changed + if (updatedItemKeys.size > 0) { + const updatedItemKeysArray = Array.from(updatedItemKeys); + queueMicrotask(() => { + itemsListeners.forEach(fn => fn(updatedItemKeysArray)); + }); + } } // Initial snapshot (sync), no notifications @@ -104,5 +129,12 @@ export function createItemsStore( const update: Store['update'] = (entries, renderItem) => apply(entries, renderItem, true); - return { getKeys, getNode, subscribeItem, subscribeKeys, update }; + return { + getKeys, + getNode, + subscribeItem, + subscribeItems, + subscribeKeys, + update + }; } diff --git a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts index 50e1eb85..728de049 100644 --- a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts @@ -1,32 +1,23 @@ -import { useCallback } from 'react'; -import type { View } from 'react-native'; +import { useCallback, useEffect } from 'react'; import type { AnimatedRef } from 'react-native-reanimated'; -import { runOnUI } from 'react-native-reanimated'; +import type Animated from 'react-native-reanimated'; +import { measure, runOnUI } from 'react-native-reanimated'; +import { useStableCallback } from '../../hooks'; import { setAnimatedTimeout, useMutableValue } from '../../integrations/reanimated'; import type { Dimensions, MeasurementsContextType } from '../../types'; +import { areValuesDifferent, resolveDimension } from '../../utils'; import { createProvider } from '../utils'; import { useCommonValuesContext } from './CommonValuesProvider'; import { useItemsContext } from './ItemsProvider'; import { useMultiZoneContext } from './MultiZoneProvider'; -type StateContextType = { - measuredItemKeys: Set; - queuedMeasurements: Map; -}; - -type MeasurementsProviderProps = { - measureDebounceDelay: number; -}; - const { MeasurementsProvider, useMeasurementsContext } = createProvider( 'Measurements' -)(({ - measureDebounceDelay -}) => { +), MeasurementsContextType>(() => { const { activeItemDimensions, activeItemKey, @@ -40,105 +31,111 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( } = useCommonValuesContext(); const { activeItemDimensions: multiZoneActiveItemDimensions } = useMultiZoneContext() ?? {}; - const { getKeys } = useItemsContext(); - - const context = useMutableValue(null); - const itemRefs = useMutableValue>>({}); - - // const handleItemMeasurement = useStableCallback( - // (key: string, dimensions: Dimensions) => { - // 'worklet'; - // runOnUI(() => { - // context.value ??= { - // measuredItemKeys: new Set(), - // queuedMeasurements: new Map() - // }; - - // const ctx = context.value; - - // const isNewItem = - // !ctx.measuredItemKeys.has(key) && - // (resolveDimension(itemWidths.value, key) === null || - // resolveDimension(itemHeights.value, key) === null); - - // if (isNewItem) { - // ctx.measuredItemKeys.add(key); - // } - - // ctx.queuedMeasurements.set(key, dimensions); - - // if (activeItemKey.value === key) { - // activeItemDimensions.value = dimensions; - // if (multiZoneActiveItemDimensions) { - // multiZoneActiveItemDimensions.value = dimensions; - // } - // } - - // // Update the array of item dimensions only after all items have been - // // measured to reduce the number of times animated reactions are triggered - // if (ctx.measuredItemKeys.size !== itemsCount) { - // return; - // } - - // const updateDimensions = () => { - // const updateDimension = ( - // dimension: Dimension, - // sizes: SharedValue - // ) => { - // const newSizes = { ...(sizes.value as Record) }; - // for (const [k, dims] of ctx.queuedMeasurements.entries()) { - // newSizes[k] = dims[dimension]; - // } - // sizes.value = newSizes; - // }; - - // if (!isWidthControlled) { - // updateDimension('width', itemWidths); - // } - // if (!isHeightControlled) { - // updateDimension('height', itemHeights); - // } - - // ctx.queuedMeasurements.clear(); - // debounce.cancel(); - // }; - - // if (isNewItem || ctx.queuedMeasurements.size === itemsCount) { - // // Update dimensions immediately to avoid unnecessary delays when: - // // - measurements were triggered because of adding new items and all new items have been measured - // // - all sortable container items' dimensions have changed (e.g. when someone creates collapsible - // // items which change their height when the user starts dragging them) - // updateDimensions(); - // } else { - // // In all other cases, debounce the update to reduce the number of - // // updates when dimensions change many times within a short period of time - // debounce.schedule(updateDimensions, measureDebounceDelay); - // } - // })(); - // } - // ); - - // const removeItemMeasurements = useCallback( - // (key: string) => { - // delete previousItemDimensionsRef.current[key]; - // const { height: isHeightControlled, width: isWidthControlled } = - // controlledItemDimensions; - // if (isWidthControlled && isHeightControlled) { - // return; - // } - - // runOnUI(() => { - // if (itemWidths.value && typeof itemWidths.value === 'object') { - // delete itemWidths.value[key]; - // } - // if (itemHeights.value && typeof itemHeights.value === 'object') { - // delete itemHeights.value[key]; - // } - // context.value?.measuredItemKeys.delete(key); - // })(); - // }, - // [controlledItemDimensions, itemHeights, itemWidths, context] - // ); + const { subscribeItems } = useItemsContext(); + + const itemRefs = useMutableValue>>( + {} + ); + + const measureItems = useCallback( + (keys: Array) => { + 'worklet'; + let hasWidthUpdates = false; + const widthUpdates: Record = {}; + + let hasHeightUpdates = false; + const heightUpdates: Record = {}; + + for (const key of keys) { + const itemRef = itemRefs.value[key]; + if (!itemRef) { + continue; + } + + const measurements = measure(itemRef); + if (!measurements) { + continue; + } + + const { height, width } = measurements; + if (!controlledItemDimensions.width) { + const resolvedWidth = resolveDimension(itemWidths.value, key); + if ( + resolvedWidth === null || + areValuesDifferent(resolvedWidth, width, 1) + ) { + widthUpdates[key] = width; + hasWidthUpdates = true; + } + } + if (!controlledItemDimensions.height) { + const resolvedHeight = resolveDimension(itemHeights.value, key); + if ( + resolvedHeight === null || + areValuesDifferent(resolvedHeight, height, 1) + ) { + heightUpdates[key] = height; + hasHeightUpdates = true; + } + } + } + + if (hasWidthUpdates) { + itemWidths.value = { + ...(itemWidths.value as Record), + ...widthUpdates + }; + } + if (hasHeightUpdates) { + itemHeights.value = { + ...(itemHeights.value as Record), + ...heightUpdates + }; + } + + const activeKey = activeItemKey.value; + if ( + activeKey && + (widthUpdates[activeKey] !== undefined || + heightUpdates[activeKey] !== undefined) + ) { + const dimensions = { + height: heightUpdates[activeKey]!, + width: widthUpdates[activeKey]! + }; + activeItemDimensions.value = dimensions; + if (multiZoneActiveItemDimensions) { + multiZoneActiveItemDimensions.value = dimensions; + } + } + }, + [ + itemRefs, + controlledItemDimensions, + itemHeights, + itemWidths, + activeItemDimensions, + multiZoneActiveItemDimensions, + activeItemKey + ] + ); + + useEffect( + () => subscribeItems(runOnUI(measureItems)), + [measureItems, subscribeItems] + ); + + const registerItem = useStableCallback( + (key: string, ref: AnimatedRef) => { + runOnUI(() => { + itemRefs.value[key] = ref; + })(); + + return runOnUI(() => { + delete itemRefs.value[key]; + }); + } + ); const handleContainerMeasurement = useCallback( (width: number, height: number) => { @@ -185,23 +182,11 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( ] ); - const resetMeasurements = useCallback(() => { - runOnUI(() => { - context.value = null; - if (typeof itemWidths.value === 'object') { - itemWidths.value = null; - } - if (typeof itemHeights.value === 'object') { - itemHeights.value = null; - } - })(); - }, [itemHeights, itemWidths, context]); - return { value: { applyControlledContainerDimensions, handleContainerMeasurement, - resetMeasurements + registerItem } }; }); diff --git a/packages/react-native-sortables/src/providers/shared/SharedProvider.tsx b/packages/react-native-sortables/src/providers/shared/SharedProvider.tsx index 5babd4c0..838cd764 100644 --- a/packages/react-native-sortables/src/providers/shared/SharedProvider.tsx +++ b/packages/react-native-sortables/src/providers/shared/SharedProvider.tsx @@ -38,7 +38,6 @@ export type SharedProviderProps = PropsWithChildren< | 'debug' | 'hapticsEnabled' | 'itemsLayoutTransitionMode' - | 'measureDebounceDelay' | 'sortEnabled' > > & @@ -63,7 +62,6 @@ export default function SharedProvider({ customHandle, debug, hapticsEnabled, - measureDebounceDelay, onActiveItemDropped, onDragEnd, onDragMove, @@ -95,7 +93,7 @@ export default function SharedProvider({ {...rest} />, // Provider used for measurements of items and the container - , + , // Provider used for auto-scrolling when dragging an item near the // edge of the container scrollableRef && ( diff --git a/packages/react-native-sortables/src/types/props/shared.ts b/packages/react-native-sortables/src/types/props/shared.ts index 1c4b1de7..33d103a0 100644 --- a/packages/react-native-sortables/src/types/props/shared.ts +++ b/packages/react-native-sortables/src/types/props/shared.ts @@ -302,11 +302,6 @@ export type SharedProps = Simplify< DropIndicatorSettings & ItemDragSettings & ItemLayoutAnimationSettings & { - /** Delay after the last item measurement and the measurements commit - * triggering the layout calculation - * @default 0 - */ - measureDebounceDelay: number; /** Whether and how to animate container dimensions changes * @default 'none' */ diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index 0ecf4cd1..970164b7 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -10,6 +10,7 @@ import type { MeasuredDimensions, SharedValue } from 'react-native-reanimated'; +import type Animated from 'react-native-reanimated'; import type { DeepReadonly, Maybe, Simplify } from '../../helperTypes'; import type { AnimatedValues } from '../../integrations/reanimated'; @@ -44,6 +45,9 @@ export type ItemsContextType = { subscribeKeys: (callback: () => void) => () => void; getNode: (key: string) => ReactNode | undefined; subscribeItem: (key: string, callback: () => void) => () => void; + subscribeItems: ( + callback: (updatedItemKeys: Array) => void + ) => () => void; }; // COMMON VALUES @@ -102,11 +106,12 @@ export type CommonValuesContextType = // MEASUREMENTS export type MeasurementsContextType = { - handleItemMeasurement: (key: string, dimensions: Dimensions) => void; - removeItemMeasurements: (key: string) => void; + registerItem: ( + key: string, + animatedRef: AnimatedRef + ) => () => void; handleContainerMeasurement: (width: number, height: number) => void; applyControlledContainerDimensions: (dimensions: Partial) => void; - resetMeasurements: () => void; }; // AUTO SCROLL diff --git a/packages/react-native-sortables/src/utils/equality.ts b/packages/react-native-sortables/src/utils/equality.ts index 94a2d9d7..42fe3760 100644 --- a/packages/react-native-sortables/src/utils/equality.ts +++ b/packages/react-native-sortables/src/utils/equality.ts @@ -14,22 +14,22 @@ export const areArraysDifferent = ( arr1.some((item, index) => !areEqual(item, arr2[index] as T)); export const areValuesDifferent = ( - dim1: number | undefined, - dim2: number | undefined, + value1: number | undefined, + value2: number | undefined, eps?: number ): boolean => { - if (dim1 === undefined) { - return dim2 !== undefined; + if (value1 === undefined) { + return value2 !== undefined; } - if (dim2 === undefined) { + if (value2 === undefined) { return true; } if (eps) { - return Math.abs(dim1 - dim2) > eps; + return Math.abs(value1 - value2) > eps; } - return dim1 !== dim2; + return value1 !== value2; }; export const areVectorsDifferent = ( From 279c7f2fada1c7ee6abda57d7f535c4b2db4f257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sun, 14 Sep 2025 17:26:17 +0200 Subject: [PATCH 3/3] Some more changes, but still getting wrong measurements when transform scale is used --- .../shared/DraggableView/DraggableView.tsx | 15 +- .../shared/DraggableView/ItemCell.tsx | 83 +++++---- .../providers/shared/CommonValuesProvider.ts | 17 +- .../providers/shared/MeasurementsProvider.ts | 168 +++++++++--------- .../src/types/providers/shared.ts | 6 +- 5 files changed, 146 insertions(+), 143 deletions(-) diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx index 710382dd..cf8041c2 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/DraggableView.tsx @@ -1,9 +1,8 @@ -import { Fragment, memo, useEffect, useState } from 'react'; +import { Fragment, memo, useCallback, useState } from 'react'; +import type { View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; -import type Animated from 'react-native-reanimated'; import { LayoutAnimationConfig, - useAnimatedRef, useDerivedValue } from 'react-native-reanimated'; @@ -39,10 +38,9 @@ function DraggableView({ }: DraggableViewProps) { const portalContext = usePortalContext(); const commonValuesContext = useCommonValuesContext(); - const { registerItem } = useMeasurementsContext(); + const { updateItemRef } = useMeasurementsContext(); const { activeItemKey, customHandle } = commonValuesContext; - const itemRef = useAnimatedRef(); const [isHidden, setIsHidden] = useState(false); const activationAnimationProgress = useMutableValue(0); const isActive = useDerivedValue(() => activeItemKey.value === key); @@ -53,7 +51,10 @@ function DraggableView({ ); const gesture = useItemPanGesture(key, activationAnimationProgress); - useEffect(() => registerItem(key, itemRef), [key, registerItem, itemRef]); + const ref = useCallback( + (instance: null | View) => updateItemRef(key, instance), + [key, updateItemRef] + ); const renderItemCell = (hidden = false) => { const innerComponent = ( @@ -66,7 +67,7 @@ function DraggableView({ isActive={isActive} itemKey={key} layoutStyleValue={layoutStyleValue} - ref={itemRef}> + ref={ref}> diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx index f789b4a2..b8800056 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/ItemCell.tsx @@ -1,5 +1,6 @@ import type { PropsWithChildren } from 'react'; -import { Platform, StyleSheet, type ViewStyle } from 'react-native'; +import type { View, ViewStyle } from 'react-native'; +import { Platform, StyleSheet } from 'react-native'; import type { SharedValue, TransformArrayItem } from 'react-native-reanimated'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; @@ -24,51 +25,49 @@ export type ItemCellProps = PropsWithChildren<{ exiting?: LayoutAnimation; }>; -const ItemCell = componentWithRef( - function ItemCell( - { - activationAnimationProgress, - baseStyle, - children, - entering, - exiting, - hidden, - isActive, - itemKey, - layoutStyleValue - }, - ref - ) { - const { controlledItemDimensionsStyle } = useCommonValuesContext(); +const ItemCell = componentWithRef(function ItemCell( + { + activationAnimationProgress, + baseStyle, + children, + entering, + exiting, + hidden, + isActive, + itemKey, + layoutStyleValue + }, + ref +) { + const { controlledItemDimensionsStyle } = useCommonValuesContext(); - const decorationStyleValue = useItemDecoration( - itemKey, - isActive, - activationAnimationProgress - ); + const decorationStyleValue = useItemDecoration( + itemKey, + isActive, + activationAnimationProgress + ); - const animatedStyle = useAnimatedStyle(() => ({ - ...decorationStyleValue.value, - ...layoutStyleValue.value, - transform: [ - ...((layoutStyleValue.value.transform ?? []) as TransformsArray), - ...((decorationStyleValue.value.transform ?? []) as TransformsArray) - ] - })); + const animatedStyle = useAnimatedStyle(() => ({ + ...decorationStyleValue.value, + ...layoutStyleValue.value, + transform: [ + ...((layoutStyleValue.value.transform ?? []) as TransformsArray), + ...((decorationStyleValue.value.transform ?? []) as TransformsArray) + ] + })); - return ( - - + return ( + + - ); - } -); + + ); +}); export default ItemCell; diff --git a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts index 6054f174..4157011b 100644 --- a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts @@ -93,10 +93,19 @@ const { CommonValuesContext, CommonValuesProvider, useCommonValuesContext } = const controlledItemHeight = useDerivedValue(() => typeof itemHeights.value === 'number' ? itemHeights.value : undefined ); - const controlledItemDimensionsStyle = useAnimatedStyle(() => ({ - height: controlledItemHeight.value, - width: controlledItemWidth.value - })); + const controlledItemDimensionsStyle = useAnimatedStyle(() => { + console.log( + 'controlledItemDimensionsStyle', + controlledItemWidth.value, + controlledItemHeight.value + ); + return { + height: controlledItemHeight.value, + maxHeight: controlledItemHeight.value, + maxWidth: controlledItemWidth.value, + width: controlledItemWidth.value + }; + }); // DRAG STATE const activeItemKey = useMutableValue(null); diff --git a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts index 728de049..62e6ba5d 100644 --- a/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/MeasurementsProvider.ts @@ -1,15 +1,10 @@ -import { useCallback, useEffect } from 'react'; -import type { AnimatedRef } from 'react-native-reanimated'; -import type Animated from 'react-native-reanimated'; -import { measure, runOnUI } from 'react-native-reanimated'; - -import { useStableCallback } from '../../hooks'; -import { - setAnimatedTimeout, - useMutableValue -} from '../../integrations/reanimated'; +import { useCallback, useEffect, useRef } from 'react'; +import type { View } from 'react-native'; +import { runOnUI } from 'react-native-reanimated'; + +import { setAnimatedTimeout } from '../../integrations/reanimated'; import type { Dimensions, MeasurementsContextType } from '../../types'; -import { areValuesDifferent, resolveDimension } from '../../utils'; +import { areValuesDifferent } from '../../utils'; import { createProvider } from '../utils'; import { useCommonValuesContext } from './CommonValuesProvider'; import { useItemsContext } from './ItemsProvider'; @@ -33,60 +28,22 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( useMultiZoneContext() ?? {}; const { subscribeItems } = useItemsContext(); - const itemRefs = useMutableValue>>( - {} - ); + const prevItemDimensionsRef = useRef>({}); + const itemRefs = useRef>({}); - const measureItems = useCallback( - (keys: Array) => { + const updateDimensionsUI = useCallback( + ( + widthUpdates: null | Record, + heightUpdates: null | Record + ) => { 'worklet'; - let hasWidthUpdates = false; - const widthUpdates: Record = {}; - - let hasHeightUpdates = false; - const heightUpdates: Record = {}; - - for (const key of keys) { - const itemRef = itemRefs.value[key]; - if (!itemRef) { - continue; - } - - const measurements = measure(itemRef); - if (!measurements) { - continue; - } - - const { height, width } = measurements; - if (!controlledItemDimensions.width) { - const resolvedWidth = resolveDimension(itemWidths.value, key); - if ( - resolvedWidth === null || - areValuesDifferent(resolvedWidth, width, 1) - ) { - widthUpdates[key] = width; - hasWidthUpdates = true; - } - } - if (!controlledItemDimensions.height) { - const resolvedHeight = resolveDimension(itemHeights.value, key); - if ( - resolvedHeight === null || - areValuesDifferent(resolvedHeight, height, 1) - ) { - heightUpdates[key] = height; - hasHeightUpdates = true; - } - } - } - - if (hasWidthUpdates) { + if (widthUpdates) { itemWidths.value = { ...(itemWidths.value as Record), ...widthUpdates }; } - if (hasHeightUpdates) { + if (heightUpdates) { itemHeights.value = { ...(itemHeights.value as Record), ...heightUpdates @@ -94,24 +51,31 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( } const activeKey = activeItemKey.value; - if ( - activeKey && - (widthUpdates[activeKey] !== undefined || - heightUpdates[activeKey] !== undefined) - ) { - const dimensions = { - height: heightUpdates[activeKey]!, - width: widthUpdates[activeKey]! + if (activeKey === null) { + return; + } + + const newHeight = + heightUpdates?.[activeKey] ?? activeItemDimensions.value?.height; + const newWidth = + widthUpdates?.[activeKey] ?? activeItemDimensions.value?.width; + + if (newHeight === undefined || newWidth === undefined) { + return; + } + + activeItemDimensions.value = { + height: newHeight, + width: newWidth + }; + if (multiZoneActiveItemDimensions) { + multiZoneActiveItemDimensions.value = { + height: newHeight, + width: newWidth }; - activeItemDimensions.value = dimensions; - if (multiZoneActiveItemDimensions) { - multiZoneActiveItemDimensions.value = dimensions; - } } }, [ - itemRefs, - controlledItemDimensions, itemHeights, itemWidths, activeItemDimensions, @@ -121,21 +85,55 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( ); useEffect( - () => subscribeItems(runOnUI(measureItems)), - [measureItems, subscribeItems] + () => + subscribeItems(itemKeys => { + const updatedWidths: Record = {}; + let hasWidthUpdates = false; + const updatedHeights: Record = {}; + let hasHeightUpdates = false; + + itemKeys.forEach(key => { + itemRefs.current[key]?.measure((_x, _y, width, height) => { + const prevDimensions = prevItemDimensionsRef.current[key]; + + if ( + !controlledItemDimensions.width && + (!prevDimensions || + areValuesDifferent(prevDimensions.width, width, 1)) + ) { + updatedWidths[key] = width; + hasWidthUpdates = true; + } + if ( + !controlledItemDimensions.height && + (!prevDimensions || + areValuesDifferent(prevDimensions.height, height, 1)) + ) { + updatedHeights[key] = height; + hasHeightUpdates = true; + } + + prevItemDimensionsRef.current[key] = { height, width }; + }); + }); + + const widthUpdates = hasWidthUpdates ? updatedWidths : null; + const heightUpdates = hasHeightUpdates ? updatedHeights : null; + + if (widthUpdates || heightUpdates) { + runOnUI(updateDimensionsUI)(widthUpdates, heightUpdates); + } + }), + [subscribeItems, itemRefs, updateDimensionsUI, controlledItemDimensions] ); - const registerItem = useStableCallback( - (key: string, ref: AnimatedRef) => { - runOnUI(() => { - itemRefs.value[key] = ref; - })(); - - return runOnUI(() => { - delete itemRefs.value[key]; - }); + const updateItemRef = useCallback((key: string, instance: null | View) => { + if (instance) { + itemRefs.current[key] = instance; + } else { + delete itemRefs.current[key]; } - ); + }, []); const handleContainerMeasurement = useCallback( (width: number, height: number) => { @@ -186,7 +184,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider( value: { applyControlledContainerDimensions, handleContainerMeasurement, - registerItem + updateItemRef } }; }); diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index 970164b7..5f20fd27 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -10,7 +10,6 @@ import type { MeasuredDimensions, SharedValue } from 'react-native-reanimated'; -import type Animated from 'react-native-reanimated'; import type { DeepReadonly, Maybe, Simplify } from '../../helperTypes'; import type { AnimatedValues } from '../../integrations/reanimated'; @@ -106,10 +105,7 @@ export type CommonValuesContextType = // MEASUREMENTS export type MeasurementsContextType = { - registerItem: ( - key: string, - animatedRef: AnimatedRef - ) => () => void; + updateItemRef: (key: string, instance: null | View) => void; handleContainerMeasurement: (width: number, height: number) => void; applyControlledContainerDimensions: (dimensions: Partial) => void; };