Reputation: 161
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.
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
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
Reputation: 10145
It doesn't work because you're not following the rules of hooks. The following things are wrong in your code:
useEffect
hook, but passing empty dependency arrayuseState
or useRef
hook so that they aren't recreated every renderThen 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