Search code examples
reactjsreact-nativereact-native-animatable

Conditionally animated component is janky


I am trying to create an InputAccessoryView for my app. It has a TextInput and a Pressable button.

What I would like to do is, when the TextInput has empty value then hide the button, else show the button using react native animation.

I have come up with the working component that works as expected. And for the animations I am utilizing useNativeDriver: true.

However, there's an issue:

  1. When text is empty and I type a word and then pause, then the showing animation is animating smoothly. (as the gif below)

enter image description here

  1. But when text is empty and I am typing continuously, then the showing animation is animating in a janky way. (as the gif below)

enter image description here

Here is the code and also link to the expo snack:

export default function App() {
  const [text, setText] = React.useState('');
  const translateY = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    if (text.length > 0) {
      // animate the add button to show by coming up
      _animateShowAddButton();
    } else {
      // animate the add button to hide by going down
      _animateHideAddButton();
    }
    // return null
  }, [text]);

  const _animateShowAddButton = () => {
    Animated.timing(translateY, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  const _animateHideAddButton = () => {
    Animated.timing(translateY, {
      toValue: 100,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  const onPressFunction = () => {
    console.log('onPressFunction');
  };

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>Some text view</Text>
      <View style={styles.myInputAccessory}>
        <TextInput
          placeholder="Enter text"
          style={styles.textInputStyle}
          onChangeText={(text) => setText(text)}
        />
        <Animated.View
          style={[
            {
              transform: [
                {
                  translateY: translateY,
                },
              ],
            },
          ]}>
          <Pressable style={styles.buttonStyle} onPress={onPressFunction}>
            <Ionicons name="send" size={24} color="white" />
          </Pressable>
        </Animated.View>
      </View>
    </View>
  );
}

It seems like the animation is paused (and animates again) whenever the text is changed. Could you help me how to animate the button to show smoothly when the text is not empty?


Solution

  • The problem is that useEffect is firing on each text change due to the second parameter [text]. So, the animation is starting on every text change.

    You can prevent this by "locking" the animation with a bit of state. Here are the relevant changes to your component:

    function App() {
      const [isAnimating, setIsAnimating] = useState(false);
      const [isVisible, setIsVisible] = useState(false);
    
      useEffect(() => {
        // only start a new animation if we're not currently animating
        if (!isAnimating) {
          // only animate show if currently invisible
          if (!isVisible && text.length > 0) {
            _animateShowAddButton();
          // only animate hide if currently visible
          } else if (isVisible && text.length === 0) {
            _animateHideAddButton();
          }
        }
      // check if we need to animate whenever text changes
      // also check whenever animation finishes
      }, [text, isAnimating]);
    
      const _animateShowAddButton = () => {
        // secure the lock so another animation can't start
        setIsAnimating(true);
        setIsVisible(true);
        Animated.timing(translateY, {
          toValue: 0,
          duration: 300,
          useNativeDriver: true,
        // release the lock on completion
        }).start(() => setIsAnimating(false));
      };
    
      const _animateHideAddButton = () => {
        // secure the lock so another animation can't start
        setIsAnimating(true);
        setIsVisible(false);
        Animated.timing(translateY, {
          toValue: 100,
          duration: 300,
          useNativeDriver: true,
        // release the lock on completion
        }).start(() => setIsAnimating(false));
      };
    }