Animated FlatList doesn’t re-render correctly when last item deleted on Android

Description

I’m trying to use an Animated FlatList using the libraries reanimated and gesture-handler from you guys. I have a horizontal list of items that have an option to be removed when you click an x in the corner.

This is working fine on iPhone, however on Android it seems the default scrolling when the last item in the list is removed stays put, displaying a non-existing empty item (leaving the component where the item was blank), instead of being re-rendered to the new last item in the updated array.

Wasn’t sure whether to report this as a bug or not, but any help with this would be much appreciated.

Code

export const useDisplayCards = () => {
   const card1 = useSelector(state => state.homeCards.card1);
   const card2 = useSelector(state => state.homeCards.card2);
   const card3 = useSelector(state => state.homeCards.card3);
   const card4 = useSelector(state => state.homeCards.card3);
   const currentUser = useSelector(state => state.user.currentUser);
   const dispatch = useDispatch();

   const [components, setComponents] = useState<Component[]>([
      {
         id: 'card1',
         comp: <View key="1" />,
         iconColor: COLORS.WHITE,
         seen: card1,
         showHideButton: false,
      },
      {
         id: 'card2',
         comp: <View key="2" />,
         iconColor: COLORS.DARK,
         seen: card2,
         showHideButton: false,
      },
      {
         id: 'card3',
         comp: <View key="3" />,
         iconColor: COLORS.DARK,
         seen: card3,
         showHideButton: true,
      },
      {
         id: 'card4',
         comp: <View key="4" />,
         iconColor: COLORS.WHITE,
         seen: card4,
         showHideButton: true,
      },
   ]);

   const scrollX = useRef(new Animated.Value(0)).current;

   useEffect(() => {
      const filterNull = components.filter(el => el.seen && el);
      setComponents(filterNull);
   }, [currentUser, card1, card2, card3, card4]);

   const renderItems = ({ item, index }: { item: Component; index: number }) => {
      const inputRange = [
         (index - 1) * DIMENSIONS.WIDTH,
         index * DIMENSIONS.WIDTH,
         (index + 1) * DIMENSIONS.WIDTH,
      ];
      const scale = scrollX.interpolate({
         inputRange,
         outputRange: [0.9, 1, 0.75],
      });
      return (
         <Animated.View style={[styles.flex, { transform: [{ scale }] }]}>
            <PaperCard
               showHideButton={item.showHideButton}
               style={{
                  marginTop: 30,
                  width: DIMENSIONS.WIDTH - 65,
               }}
               iconColor={item.iconColor || COLORS.DARK}
               hideCard={() => {
                  const updatedItems = components.filter((_item, itemIndex) => itemIndex !== index);
                  setComponents(updatedItems);
                  dispatch(hideCard(item.id));
               }}>
               {item.comp}
            </PaperCard>
         </Animated.View>
      );
   };

   const pagination = (data: Component[]) => {
      const inputRange = [-DIMENSIONS.WIDTH - 65, 0, DIMENSIONS.WIDTH - 65];
      const translateX = scrollX.interpolate({
         inputRange,
         outputRange: [-40, 0, 40],
      });
      return (
         <View style={styles.pagination}>
            <Animated.View style={[styles.paginationDotActive, { transform: [{ translateX }] }]} />
            {data.map(item => {
               return (
                  <View key={item.id} style={styles.paginationContainer}>
                     <View style={styles.paginationDot} />
                  </View>
               );
            })}
         </View>
      );
   };

   return [components, scrollX, pagination, renderItems] as const;
};
____________________________
export type PaperCardProps = {
   style?: ViewStyle;
   navigate?(): void;
   children: ReactNode;
   hideCard?(): void;
   iconColor?: string;
   showHideButton?: boolean;
   onPress?(): void;
};
const PaperCard: FunctionComponent<PaperCardProps> = ({
   style,
   navigate,
   children,
   hideCard,
   iconColor,
   showHideButton = false,
   onPress,
}) => {
   return (
      <TouchableOpacity onPress={onPress} activeOpacity={1}>
         {showHideButton && (
            <IconButton
               style={styles.iconButton}
               onPress={hideCard}
               iconStyle={styles.iconStyle}
            />
         )}
         <TouchableOpacity onPress={navigate} style={[styles.container, style]} activeOpacity={1}>
            {children}
         </TouchableOpacity>
      </TouchableOpacity>
   );
};

export default PaperCard;
______________________________
export const hideCard = (card: string) => async (dispatch: AppDispatch) => {
   dispatch({
      type: HIDE_HOME_CARD,
      payload: { card },
   });
};
______________________________

const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);

const HomeScreen = FunctionComponent<> = () => {
  const dispatch = useDispatch();
  const currentUser = useSelector(state => state.user.currentUser);
  const [components, scrollX, pagination, renderItems] = useDisplayCards();

  const list = (
      <View style={styles.paperCardContainer}>
         <AnimatedFlatList
            showsHorizontalScrollIndicator={false}
            contentContainerStyle={{ paddingRight: 40, paddingLeft: 20 }}
            extraData={components}
            data={components}
            keyExtractor={(item: Component, index: number) => `${index} ${item}`}
            renderItem={renderItems}
            horizontal
            snapToInterval={DIMENSIONS.WIDTH - 65}
            decelerationRate="fast"
            onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } } }], {
               useNativeDriver: true,
            })}
            scrollEventThrottle={16}
         />
         {components.length > 1 ? pagination(components) : null}
      </View>
   );

 return (
            <ScrollBottomSheet
               ...
               ListHeaderComponent={list}
              ....
            />
 );
}

Package versions

  • React Native: 0.63.3
  • React Native Reanimated: 1.13.1
  • React Native Gesture Handler : 1.7.0

1 possible answer(s) on “Animated FlatList doesn’t re-render correctly when last item deleted on Android

  1. Could you please add a full (copy-pastable, without external deps) example so I can test this locally? You didn’t attach dispatch() function, nor hideItem().

    At the first glance, I see you’re using useRef with Animated.Value, this won’t work (you’re creating a new instance of Value that is not used, but it initializes the native node), use useMemo with function or useValue from Reanimated instead.