reactjsreact-nativereact-navigationreact-animated

How to re-render screen on click in React Native Navigation?


I have a rectangle inside React Native Navigation screen that I want to animate every time user clicks on that specific screen. Now it only plays animation once, when application renders for the first time. I saw multiple solutions that worked for other people for this kind of a problem, but none of them worked for me - the application doesn't re-render on click. What am I doing wrong?

SettingsScreen.js (animation which needs to rerender on screen switch)

const { width } = Dimensions.get("screen");

const SettingsScreen = ({navigation}) => {
  const isFocused = useIsFocused();
  return (
    <FlatList
      contentContainerStyle={style.barContainer}
      data={[1, 2, 3, 4, 5]}
      keyExtractor={(_, index) => index.toString()}
      renderItem={() => (
        <ProgressBar isFocused={isFocused} navigation={navigation} />
      )}
    ></FlatList>
  );
};

const ProgressBar = ({ navigation, isFocused }) => {
  const barWidth = React.useRef(new Animated.Value(0)).current;

  console.log(barWidth);

  const finalWidth = width / 2;

  React.useEffect(() => {
    const listener = navigation.addListener("focus", () => {
      Animated.spring(barWidth, {
        toValue: finalWidth,
        bounciness: 10,
        speed: 2,
        useNativeDriver: false,
      }).start();
    });

    return listener;
  }, [navigation]);

  return (
    <View style={style.contentContainer}>
      <Animated.View style={[style.progressBar, { width: barWidth }]} />
    </View>
  );
};

MainContainer.js (where all the navigation is set up)

const profileName = "Profile";
const detailsName = "Details";
const settingsName = "Settings";
const profileNameFR = "P";
const detailsNameFR = "D";
const settingsNameFR = "S";

const Tab = createBottomTabNavigator();

export default function MainContainer() {
  const { locale } = useContext(LanguageContext);
  
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            let rn = route.name;

            if (rn === profileName || rn === profileNameFR) {
              iconName = focused ? "person" : "person-outline";
            } else if (rn === detailsName || rn === detailsNameFR) {
              iconName = focused ? "list" : "list-outline";
            } else if (rn === settingsName || settingsNameFR) {
              iconName = focused ? "settings" : "settings-outline";
            }

            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: "tomato",
          inactiveTintColor: "grey",
          tabBarStyle: { padding: 10, height: 60 },
          tabBarLabelStyle: { paddingBottom: 10, fontSize: 10 },
          style: { padding: 10 },
        })}
      >  

Solution

  • This is happening because FlatList is a PureComponent, meaning that it will not re-render if props remain the same. So in this case try to use useIsFocused hook inside the SettingsScreen component and pass it to ProgressBar as a prop.

    Inside SettingsScreen:

    const isFocused = useIsFocused();
    
    ...
    
    <FlatList
     ...
     renderItem={() => <ProgressBar isFocused={isFocused} />}
    

    Also it's not recommended to use useIsFocused hook because this will unnecessary re-render your component many times. Instead you can use an event listener to listen to 'focus' event, like this:

    React.useEffect(() => {
     const listener = navigation.addListener('focus', () => {
        Animated.spring(barWidth, {
          toValue: finalWidth,
          bounciness: 10,
          speed: 2,
          useNativeDriver: false,
        }).start();
     });
    
     return listener;
    }, [navigation])