I'm building a React Native app with a stack of cards that users can swipe through. The card at the top of the stack is animated off-screen when swiped, and the next card in the stack becomes the new top card.
Everything works as expected, except for a flickering issue: after a swipe animation completes, the card that was just swiped briefly reappears at the top before disappearing. Here's what I'm doing:
Key Features:
Stack Structure: The card stack is managed using an array of post IDs (cardOrder), where the top card is always at cardOrder[0].
Animation: The swipe is implemented with Animated.ValueXY for position and PanResponder for drag gestures.
Reorder: When a card is swiped off the screen, the array is shifted so that the card's id goes from the first element to the last
Key Prop: I use a combination of postID and index for the key prop in the map function.
My Code:
Here’s the relevant part of my code for rendering the stack:
{cardOrder.map((postID, index) => {
const isTopCard = index === 0;
const post = posts.find((p) => p.id === postID);
return (
<Animated.View
{...(isTopCard ? panResponder.panHandlers : {})}
key={`${postID}-${index}`}
style={[
styles.card,
{
opacity: index < 5 ? 1 : 0, // Hide cards beyond the top 5
top: height * 0.2,
transform: [
{ translateX: isTopCard ? position.x : 8 * index },
{ translateY: isTopCard ? position.y : 0 },
],
},
]}
>
<Post post={post} />
</Animated.View>
);
})}
Here is the relevant code from the panResponder:
onPanResponderRelease: (_, gesture) => {
// when the user releases the card, check if it should be swiped off the screen
if (gesture.dx > 120 || gesture.dx < -120) {
const offScreenX = gesture.dx > 0 ? width + 100 : -width - 100;
Animated.timing(position, {
toValue: { x: offScreenX, y: gesture.dy },
duration: 300,
useNativeDriver: false,
}).start(() => {
handleSwipe(); // move the top card to the back of the stack and next card to the top
});
Here is the relevant part of the handleSwipe function:
// update the card order
setCardOrder((prevOrder) => {
const newOrder = [...prevOrder];
newOrder.push(newOrder.shift()); // Move the top card to the end
return newOrder;
});
// reset the position of the top card
position.setValue({ x: 0, y: 0 });
What I’ve Tried:
- Using unique postID as the key prop alone.
Result: When I swipe a card, I move the top card's postID from index 0 to the end of the cardOrder array. React sees that the same key (postID) is now at a different position and optimizes by reusing the component, which causes visual glitches such as flickering and a second stack appearing.
- Using index as the key prop instead of postID.
Result: Flickering stops, but the stack breaks when the card order updates dynamically.
My Hypothesis:
I suspect the issue is related to how React Native re-renders components when the key prop changes, possibly causing the top card to momentarily reappear. The whole problem is that the card shift, apparently, happens after the animation, resulting in the swiped card momentarily appearing as the top card before disappearing. That's what makes it visually not appealing.
Question:
Is there a way to make the card that is swiped not appear again as the top card after the animation?
Any guidance or insights would be appreciated! Let me know if more code or context is needed.