skip to Main Content

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.

2

Answers


  1. Here are a few approaches to try to sync state updates with React Native animated values updates:

    Sequence updates:

    Call setState to update state
    In the state update callback, update animated values
    This sequences them so state update finishes before animated values.

    Use LayoutEffect:

    Call setState
    In the next render, use useLayoutEffect to update animated values
    LayoutEffect fires synchronously after render, so values should be in sync.

    Batch updates:

    Combine state and animated value updates into one batched operation using React Native’s Animated.batch()
    This ensures they happen atomically.

    Animated.batch(() => {
      setState({...});
    
      value.setValue(newValue);
    });
    

    Use requestAnimationFrame:

    Call setState
    In next render, use requestAnimationFrame to schedule animated update
    RAF commits changes on next frame so they should be in sync.

    Move animated logic to effect:

    Call setState
    Move animated logic/transition to useEffect
    Add dependencies so it only runs after state update
    Some other things to try:

    Simplify updates to remove unnecessary re-renders
    Add shouldComponentUpdate check
    Consider useReducer for state updates
    The key is to sequence or batch the updates so they commit changes atomically without races.

    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search