software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
9.05k stars 1.31k forks source link

[3.7.0] shared value array fails to be updated in multiple `onLayout`s #5735

Closed yayvery closed 8 months ago

yayvery commented 8 months ago

Description

My app has a tabs bar component that uses onLayout to record the dimensions of each individual tab item. The onLayout handler updates a SharedValue<LayoutRectangle[]>, assigning the tab item's index in the array to it's dimensions like so:

function useTabsState({ items }: UseTabsStateOptions) {
  const itemDimensions = useSharedValue<LayoutRectangle[]>([]);
  const setItemDimensions = useCallback(
    (index: number, dimensions: LayoutRectangle) => {
      // Assign first, then copy the array to a new object to tell reanimated that it has changed.
      itemDimensions.value[index] = dimensions;
      // If the number of items shrinks, can prune off invalid index item dimensions.
      itemDimensions.value = [...itemDimensions.value].slice(0, items.length);
      console.log(`--${index}--`);
      console.log('dimensions:', dimensions);
      console.log('itemDimensions:', itemDimensions.value);
    },
    [itemDimensions, items.length]
  );

  return useMemo(
    () => ({
      items,
      itemDimensions,
      setItemDimensions,
    }),
    [items, itemDimensions, setItemDimensions]
  );
}

function TabItem({ label, index, state }: TabItemProps) {
  const { setItemDimensions } = state;

  function handleLayout(event: LayoutChangeEvent) {
    setItemDimensions(index, event.nativeEvent.layout);
  }

  return (
    <Pressable
      style={styles.item}
      onLayout={handleLayout}
      accessibilityRole="tab">
      <Text numberOfLines={1}>{label}</Text>
    </Pressable>
  );
}

In Reanimated 3.7.0+, the shared value array never changes from the initial empty array []. Before 3.7.0, the array would be updated with each tab index's dimensions as expected.

I made a minimal reproducer of the bug in the Reanimated example project and I bisected the introduction of the bug to Introduce executeOnUIRuntimeSync and use it to replace Sync Data Holder #4300.

Using the reproducer, before https://github.com/software-mansion/react-native-reanimated/pull/4300, the console.log('itemDimensions:', itemDimensions.value) in useTabsState would print five different arrays, once for each TabItem onLayout.

e.g.:

itemDimensions: [{width: 1, height: 1, x: 0, y: 0}]
itemDimensions: [{width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}]
itemDimensions: [{width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}]
itemDimensions: [{width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}]
itemDimensions: [{width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0} {width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}, {width: 1, height: 1, x: 0, y: 0}]

After https://github.com/software-mansion/react-native-reanimated/pull/4300, the shared value array fails to be updated, and it prints an empty array five times:

itemDimensions: []
itemDimensions: []
itemDimensions: []
itemDimensions: []
itemDimensions: []

Steps to reproduce

see description

Snack or a link to a repository

https://github.com/yayvery/react-native-reanimated/commit/ba8a22eed862d0ae2ca0d74ac158a8ab82c29a3a

Reanimated version

3.6.0 (this is the version in package.json at https://github.com/software-mansion/react-native-reanimated/commit/d80936495f51f9884e25c3f16d0fa2983929138a)

React Native version

0.72.6 (this is the version in package.json at https://github.com/software-mansion/react-native-reanimated/commit/d80936495f51f9884e25c3f16d0fa2983929138a)

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Paper (Old Architecture)

Build type

All types (debug and release)

Device

Real device

Device model

All devices (iOS and Android)

Acknowledgements

Yes

yayvery commented 8 months ago

I was able to resolve this issue by adding 'worklet'; to setItemDimensions and calling runOnUI(setItemDimensions)(...) in handleLayout. Will leave open for now since I'm not sure if this impact was expected/intended from https://github.com/software-mansion/react-native-reanimated/pull/4300

tjzel commented 8 months ago

Hi @yayvery. You should never use this pattern

itemDimensions.value[index] = dimensions;

as per our documentation.

For complex objects you should try to use modify function instead:

itemDimensions.modify((value) => {
  'worklet';
  value[index] = dimensions;
  value = value.slice(0, items.length);
  return value;
});