Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android-IOS] [3.11.0] Animation update incorrect when setState after set shared value #6002

Closed
ngocle2497 opened this issue May 10, 2024 · 5 comments
Labels
Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS

Comments

@ngocle2497
Copy link

ngocle2497 commented May 10, 2024

Description

When i call setState after update reanimated value, animated view update incorrect
Reanimared 3.8.1 work correctly for both android and ios
Case 1: use useAnimatedStyle -> Animated view stuck on multiple view
Case 2: use inline style -> Animated view delay update

  • Android video:
Screen.Recording.2024-05-10.at.11.27.24.online-video-cutter.com.mp4
  • IOS video:
Screen.Recording.2024-05-10.at.11.14.36.online-video-cutter.com.1.mp4

Steps to reproduce

  1. Copy my code to your project
import React, {useEffect, useState} from 'react';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import Animated, {
  SharedValue,
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const Item = ({
  position,
  index,
  onPress,
}: {
  position: SharedValue<number>;
  index: number;
  onPress: (index: number) => void;
}) => {
  const handlePress = () => {
    onPress(index);
  };

  const style = useAnimatedStyle(() => {
    return {
      backgroundColor: position.value === index ? '#3498db' : 'transparent',
      borderBottomColor: position.value === index ? '#3498db' : 'transparent',
    };
  });

  return (
    <TouchableOpacity onPress={handlePress}>
      <View style={styles.item}>
        <Animated.View style={[StyleSheet.absoluteFillObject, style]} />
        <Text>Item: {index}</Text>
      </View>
    </TouchableOpacity>
  );
};

const App = () => {
  const position = useSharedValue(0);
  const [selectedIndex, setSelectedIndex] = useState<number>(position.value);

  const onPress = (index: number) => {
    position.value = index;
    setSelectedIndex(index);
  };

  const renderItem = (item: number) => {
    return (
      <Item position={position} key={item} index={item} onPress={onPress} />
    );
  };

  useEffect(() => {
    // TODO: call api or some effect
  }, [selectedIndex]);

  return (
    <View style={styles.container}>
      <View style={styles.rowItem}>{[0, 1, 2, 3, 4, 5].map(renderItem)}</View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  rowItem: {
    flexDirection: 'row',
    columnGap: 4,
    width: '100%',
    height: 100,
  },
  item: {
    height: 100,
    width: 50,
    borderBottomWidth: 2,
    borderColor: 'transparent',
    overflow: 'hidden',
  },
});

export default App;

Run your app

Snack or a link to a repository

See my code below

Reanimated version

3.11.0

React Native version

0.73.8

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Paper (Old Architecture)

Build type

Debug app & dev bundle

Device

iOS simulator

Device model

Iphone 15 Pro Max

Acknowledgements

Yes

@github-actions github-actions bot added Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Missing repro This issue need minimum repro scenario labels May 10, 2024
Copy link

Hey! 👋

The issue doesn't seem to contain a minimal reproduction.

Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?

@szydlovsky
Copy link
Contributor

@ngocle2497 Hello! This is a really interesting issue. Let me explain it:
In your example, setting a value of a SharedValue is a asynchronous operation - its value change is scheduled and it takes around 1-2 frames to get the value on both threads. Meanwhile, you change the React state immediately after it, expecting that the new render (caused by the changed state) will contain the new SharedValue value - which isn't there because it is scheduled a little bit later.
Such situations can be nicely fixed by usage of react-native-gesture-handler, which offers handling the gestures reactively and is fully integrated with Reanimated. I have re-written your example using it (keep in mind that your app needs to have a GestureHandlerRootView at its root for the gestures to work - see GH installation guide):

Fixed example code:

Code
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
  SharedValue,
  useAnimatedStyle,
  useSharedValue,
  runOnJS,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const Item = ({
  position,
  index,
}: {
  position: SharedValue<number>;
  index: number;
}) => {
  const style = useAnimatedStyle(() => {
    return {
      backgroundColor: position.value === index ? '#3498db' : 'transparent',
      borderBottomColor: position.value === index ? '#3498db' : 'transparent',
    };
  });

  return (
    <View style={styles.item}>
      <Animated.View style={[StyleSheet.absoluteFillObject, style]} />
      <Text>Item: {index}</Text>
    </View>
  );
};

const App = () => {
  const position = useSharedValue(0);
  const [selectedIndex, setSelectedIndex] = useState<number>(position.value);

  const renderItem = (item: number) => {
    const tap = Gesture.Tap().onEnd(() => {
      position.value = item;
      runOnJS(setSelectedIndex)(item);
    });

    return (
      <GestureDetector gesture={tap}>
        <Item position={position} key={item} index={item} />
      </GestureDetector>
    );
  };

  useEffect(() => {
    // TODO: call api or some effect
  }, [selectedIndex]);

  return (
    <View style={styles.container}>
      <View style={styles.rowItem}>{[0, 1, 2, 3, 4, 5].map(renderItem)}</View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  rowItem: {
    flexDirection: 'row',
    columnGap: 4,
    width: '100%',
    height: 100,
  },
  item: {
    height: 100,
    width: 50,
    borderBottomWidth: 2,
    borderColor: 'transparent',
    overflow: 'hidden',
  },
});

export default App;

@ngocle2497
Copy link
Author

@szydlovsky thank for explain. I discovered this issue when running the project on production environment. Actually, it's not just an example, the example just describes how to make an error. in my project i have 1 swipeable list. When entering a menu in a swipeable section, I also use a gesture handler (tap gesture) to close the swipeable action after switching screens. And you know, when switching screens, setstate happens very often (turn on loading, update data,...). that is swipeable and not closeable. I will upload a video of my today's production project around today (GMT +7). Hope you can fix this error for me. Thank

@ngocle2497
Copy link
Author

ngocle2497 commented May 13, 2024

RPReplay_Final1715636546.mp4

https://github.com/software-mansion/react-native-reanimated/assets/43195241/ddb4518e-547d-495c-a66c-9178c8cb8b36
I also use react navigation native stack
@szydlovsky can u open this issue by my video?
issue 1: Tab alert not active
Issue 2: swipeable cannot closeable

@szydlovsky
Copy link
Contributor

szydlovsky commented May 15, 2024

@ngocle2497 Hi again. Unfortunately, the example video is not enough te re-open the issue. I know that we cannot access the source code, because it is a private company project, but that leaves us with only guessing what could be wrong. You can try making a more complex repro out of it and posting it here - then I will reopen the issue. Other than that, I can give you some tips to check in your code:

  • as I said previously, setting the SharedValue value is a scheduled, asynchronous operation - so any renders directly afterwards may not use the changed value. If the value is being changed as a result of some tap - then you can do it in react-native-gesture-handler gesture callback, which makes it run on UI thread and execute synchronously.
  • there is one more, a bit niche way to fix such stuff - you can wrap the value setting in Reanimated's executeOnUIRuntimeSync, which, as the name suggests, runs the value changing synchronously on UI thread
  • there is always a third way - if you would like us to take a peek into the code (assuming a sufficient NDA) you could contact us through our business contact form: https://swmansion.com/contact/projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS
Projects
None yet
Development

No branches or pull requests

2 participants