question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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 = useValue(0);

   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 = interpolateNode(scrollX, {
         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 = interpolateNode(scrollX, {
         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

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:2
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
jakub-gonetcommented, Dec 4, 2020

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.

0reactions
helgifeiticommented, Feb 24, 2021

Just updated to Reanimated v2, but the problem still persists.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Animated FlatList doesn't re-render correctly when last item ...
This is working fine on iPhone, however on Android it seems the default scrolling when the last item in the list is removed...
Read more >
React Native FlatList last item visibility issue - Stack Overflow
We embed the FlatList component within our native UIs inside a Fragment in Android and we were unable to scroll to the last...
Read more >
How to use the FlatList Component — React Native Basics
What is the FlatList component? It's an easy way to make an efficient scrolling list of data. Not only is it efficient but...
Read more >
Optimizing React Native performance - LogRocket Blog
The FlatList component renders only the items that will be displayed on the screen and removes them when they are no longer displayed....
Read more >
Optimizing Flatlist Configuration - React Native
The more complex your components are, the slower they will render. Try to avoid a lot of logic and nesting in your list...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found