Élie
Élie

Reputation: 161

Animation won't start if state changed (short snack for demonstration)

Snack is here

Hello, I'm hard stuck on a silly problem and I'm becoming nut.

I just wanted to make a simple and elegant animation when a screen is focused (in a tab bar navigation). My snack works perfectly until I perform a state change in my screen. Then the animation just won't start, even though the callback from focus listener is called and executed (check logs)... WHY?

I made a button to trigger manually the animation... and it works!???? I think I made the snack clear, but if you need more information, please ask me. I beg you, please help a brother in despair.

Snack is here

If you're lazy to click the Snack:

import React, { useState, useEffect } from "react";
import { Text, View, Animated, Dimensions, StyleSheet, SafeAreaView, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function HomeScreen({navigation}) {
  const initialXPos = Dimensions.get("window").height * 0.5 ;
  const xPos = new Animated.Value(initialXPos);
  const opacity = new Animated.Value(0);
  const [change, setChange] = useState(true)

   useEffect(() => {
    const unsubscribe = navigation.addListener("focus", comingFromBot);
    return unsubscribe;
  }, []);

  const comingFromBot = () => {
    xPos.setValue(initialXPos);
    opacity.setValue(0);
    Animated.parallel([
      Animated.spring(xPos, {
        toValue: 100,
        tension:3,
        useNativeDriver: true,
      }),
      Animated.timing(opacity, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }),
    ]).start();
    console.log("Animation's Fired!");
  };

  return (
    <SafeAreaView style={{flex:1}}>
      <Animated.View style={[
            styles.container,
            { transform: [{ translateY: xPos }] },
            { opacity: opacity },
          ]}>
        <Text style={{fontSize:30}}>{change ? "Home!" : "TIMMY!"}</Text>
      </Animated.View>

      {/* debug */}
      <View style={styles.fire}>
        <Button title="fire" onPress={() => comingFromBot()}/>
      </View>

      <View style={styles.change}>
        <Button title="change" onPress={() => setChange(!change)}/>
      </View>
    </SafeAreaView>
  );
}
const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center' },
  fire:{position:"absolute", width:"100%", bottom:0},
  change:{position:"absolute", width:"100%", bottom:48}
  });

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding:8 }}>
      <Text>{"Go to Home tab again, and notice the animation.\n\nEXCEPT if we changed the text... WHY?\n\nBUT still works if we fire the animation with the button, but after still won't work on focus detection... HOW?\n\nWorks if you hot reload / hard reload the app... HELP?"}</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

Upvotes: 1

Views: 1242

Answers (2)

&#201;lie
&#201;lie

Reputation: 161

I finally ended with this, thanks to @satya164: Snack

I also wish I read this in documentation before.

HomeScreen's code:

// HomeScreen.js
function HomeScreen({navigation}) {
  const initialXPos = Dimensions.get("window").height * 0.5 ;
  const xPos = useRef(new Animated.Value(initialXPos)).current 
  const opacity = useRef(new Animated.Value(0)).current 
  const [change, setChange] = useState(true)

  useEffect(() => {
    const unsubscribe = navigation.addListener("focus", comingFromBot);
    return unsubscribe;
  }, [navigation, comingFromBot]);

  const comingFromBot = useCallback(() => {
    xPos.setValue(initialXPos);
    opacity.setValue(0);
    Animated.parallel([
      Animated.spring(xPos, {
        toValue: 100,
        tension:3,
        useNativeDriver: true,
      }),
      Animated.timing(opacity, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }),
    ]).start();
    console.log("Animation's Fired!");
  }, [xPos, opacity, initialXPos ]);

  return (
    <SafeAreaView style={{flex:1}}>
      <Animated.View style={[
            styles.container,
            { transform: [{ translateY: xPos }] },
            { opacity: opacity },
          ]}>
        <Text style={{fontSize:30}}>{change ? "Home!" : "TIMMY!"}</Text>
      </Animated.View>

      {/* debug */}
      <View style={styles.fire}>
        <Button title="fire" onPress={() => comingFromBot()}/>
      </View>

      <View style={styles.change}>
        <Button title="change" onPress={() => setChange(!change)}/>
      </View>
    </SafeAreaView>
  );
}

Upvotes: 0

satya164
satya164

Reputation: 10145

It doesn't work because you're not following the rules of hooks. The following things are wrong in your code:

  1. You're using variables from outside in useEffect hook, but passing empty dependency array
  2. The animated values need to be in a useState or useRef hook so that they aren't recreated every render

Then the animation just won't start, even though the callback from focus listener is called and executed (check logs)... WHY?

The problem is that the callback is recreated after state update on re-render, so the callback passed to the focus listener isn't the same as what's in render anymore. And since you also don't have your animated values in state/ref, new animated values are also created while the old focus listener is referring to the old values. Basically the log you see is from an old listener and not the new one.

You should use the official eslint plugin and ensure that you fix all the warnings/errors from it so that such problems are avoided.

To fix your code, do the following changes:

const [xPos] = React.useState(() => new Animated.Value(initialXPos));
const [opacity] = React.useState(() => new Animated.Value(0));
const [change, setChange] = useState(true)

useEffect(() => {
  const unsubscribe = navigation.addListener("focus", comingFromBot);
  return unsubscribe;
}, [navigation, comingFromBot]);

const comingFromBot = useCallback(() => {
  xPos.setValue(initialXPos);
  opacity.setValue(0);
  Animated.parallel([
    Animated.spring(xPos, {
      toValue: 100,
      tension:3,
      useNativeDriver: true,
    }),
    Animated.timing(opacity, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true,
    }),
  ]).start();
  console.log("Animation's Fired!");
}, [xPos, opacity]);

I basically added useCallback, fix the dependency arrays, and moved the animated values to useState hook.

Upvotes: 3

Related Questions