2

If you need to simultaneously

  • update some states such that the UI re-renders
  • and update some animation variables (useSharedValue, useAnimatedStyle etc.)

what’s the best pattern for this?

I tried the following:

  • The same code updates the state and simultaneously assigns new values to the Shared Values.
  • Update the state, then in the render method, use an useEffect to change the Shared Values.
  • Update the state, then in the render method, use a setTimeout(..., 0) to change the Shared Values.

Regardless of which one I use, I always get a short time where the UI is rendered in an invalid state - for example, the newly rendered UI is there, but the Shared Values are still old. Or, the Shared Values get updated before the UI render is finished. This results in ugly flickers in the UI. These flickers are not deterministic. They happen sometimes, but sometimes not. It seems there is some kind of race condition.

Before I start to analyze this further, what is the “correct” way to do it, from a theoretical standpoint? How can I sync these two updates such that both changes get visible at the same time?

EDIT: Since I still do not get any answers, I spent one more day isolating the problem. Seems I now have sort of a repro:

https://github.com/yolpsoftware/rea-3-bugrepro-838

If is isn't possible to sync the UI and JS thread in such a situation, we are also welcoming workarounds to solve the problem. Please see the README of the repo for requirements for a solution.

Jonas Sourlier
  • 13,684
  • 16
  • 77
  • 148
  • can you please add a snack link of the issue? – Rohit S K Aug 21 '23 at 16:04
  • @RohitSK I could. However, it seems very difficult to isolate the problem. Seems there are racing conditions that are visible only when the render method is enough heavy. As soon as I simplify my code, the problem goes away, or changes to something else that is easily solvable. I already spent days trying to isolate it. So, as I was saying in the last paragraph, I'm looking for an indication as to how this **should** work, from a theoretical standpoint. How can I make sure that Reanimated variables are updated exactly when my render changes become visible? – Jonas Sourlier Aug 21 '23 at 19:47

1 Answers1

1

Here is my approach:

at give time render 3 elements on the screen, 2 of them will be visible on screen and one will be rendered outside the screen, the third screen will be used to animate when the first element is panned, so that we don't see empty screen.

the entire project can be found here: GitHub Link

values:

  const isPanning = useSharedValue(false);
  const activeIndex = useSharedValue(0);
  const posY = useSharedValue(0);
  const scondPosY = useSharedValue(0);
  const thirdPosY = useSharedValue(-windowHeight / 2);

create pan gesture handler by using the new gesture detector from gesture handler:

const handler = Gesture.Pan()
    .onStart(() => {
      isPanning.value = true;
    })
    .onChange((event) => {
      posY.value = event.translationY;
    })
    .onEnd(() => {
      isPanning.value = false;
      if (posY.value > 50) {
        posY.value = withTiming(windowHeight);
        scondPosY.value = withTiming(windowHeight / 2);
        activeIndex.value += 1;
        thirdPosY.value = withTiming(0, {}, (finished) => {
          if (finished) {
            runOnJS(renderNextItems)(activeIndex.value);
          }
        });
      } else {
        posY.value = withTiming(0);
      }
    });

Note: above the index is changed only when animation is finished so that we can change the rendered items, before updating the state to render next set of elements reset all the animation values

file:

export const Screen = (props: Props) => {
  const [itemsToRender, setItemsToRender] = useState<string[]>(
    props.items.slice(0, 3)
  );

  // Layout stuff, gets copied from the LayoutHelper.getStyles style object
  const size = useWindowDimensions();
  const windowHeight = size.height;
  const isPanning = useSharedValue(false);
  const activeIndex = useSharedValue(0);
  const posY = useSharedValue(0);
  const scondPosY = useSharedValue(0);
  const thirdPosY = useSharedValue(-windowHeight / 2);

  const renderNextItems = useCallback((value: number) => {
    posY.value = 0;
    scondPosY.value = 0;
    thirdPosY.value = -windowHeight / 2;
    setItemsToRender(props.items.slice(value, value + 3));
  }, []);

  const handler = Gesture.Pan()
    .onStart(() => {
      isPanning.value = true;
    })
    .onChange((event) => {
      posY.value = event.translationY;
    })
    .onEnd(() => {
      isPanning.value = false;
      if (posY.value > 50) {
        posY.value = withTiming(windowHeight);
        scondPosY.value = withTiming(windowHeight / 2);
        activeIndex.value += 1;
        thirdPosY.value = withTiming(0, {}, (finished) => {
          if (finished) {
            runOnJS(renderNextItems)(activeIndex.value);
          }
        });
      } else {
        posY.value = withTiming(0);
      }
    });

  const currentItemTransform = useAnimatedStyle(
    () => ({
      transform: [
        {
          translateY: windowHeight / 2 + posY.value,
        },
      ],
    }),
    [itemsToRender.join(", ")]
  );

  const nextItemTransform = useAnimatedStyle(
    () => ({
      transform: [
        {
          translateY: scondPosY.value,
        },
      ],
    }),
    [itemsToRender.join(", ")]
  );

  const thirdTransform = useAnimatedStyle(
    () => ({
      transform: [
        {
          translateY: thirdPosY.value,
        },
      ],
    }),
    [itemsToRender.join(", ")]
  );

  const itemStyles: any[] = [];

  if (itemsToRender[0]) {
    itemStyles.push([
      styles.item,
      {
        transform: [
          {
            translateY: windowHeight / 2,
          },
        ],
      },
      currentItemTransform,
    ]);
    if (itemsToRender[1]) {
      itemStyles.push([styles.item, nextItemTransform]);
    }
    if (itemsToRender[2]) {
      itemStyles.push([styles.item, thirdTransform]);
    }
  }
  return (
    <View style={styles.container}>
      <GestureDetector gesture={handler}>
        <Animated.View style={styles.itemContainer}>
          {(itemsToRender.length === 3
            ? [2, 1, 0]
            : itemsToRender.length === 1
            ? [0]
            : []
          ).map((i) => {
            const style = itemStyles[i];
            return (
              <Animated.View key={itemsToRender[i]} style={style}>
                <Text style={styles.text}>{itemsToRender[i]}</Text>
              </Animated.View>
            );
          })}
        </Animated.View>
      </GestureDetector>
    </View>
  );
};

here is the example gif how it will work when there is load in js thread:

enter image description here

Ashwith Saldanha
  • 1,700
  • 1
  • 5
  • 15