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
Could you please add a full (copy-pastable, without external deps) example so I can test this locally? You didn’t attach
dispatch()
function, norhideItem()
.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), useuseMemo
with function oruseValue
from Reanimated instead.