I am going to implement a component that satisfies the following requirements:
- Create a two column
FlatList
whose items are your cards.
- Implement a gesture handling that recognizes
swipeLeft
and swipeRight
actions which will remove the card that was swiped.
- The
swipe
actions should be animated, meaning we have some kind of drag of the screen
behavior.
I will use the basic react-native FlatList
with numColumns={2}
and react-native-swipe-list-view to handle swipeLeft
and swipeRight
actions as well as the desired animations.
I will implement a fire and forget
action, thus after removing an item, it is gone forever. We will implement a restore mechanism
later if we want to be able to restore removed items.
My initial implementation works as follows:
- Create a
FlatList
with numColumns={2}
and some additional dummy styling to add some margins.
- Create state using
useState
which holds an array of objects that represent our cards.
- Implement a function that removes an item from the state provided an id.
- Wrap the item to be rendered in a
SwipeRow
.
- Pass the
removeItem
function to the swipeGestureEnded
prop.
import React, { useState } from "react"
import { FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"
const data = [
{
id: "0",
title: "Title 1",
},
{
id: "1",
title: "Title 2",
},
{
id: "2",
title: "Title 3",
},
{
id: "3",
title: "Title 4",
},
{
id: "4",
title: "Title 5",
},
{
id: "5",
title: "Title 6",
},
{
id: "6",
title: "Title 7",
},
{
id: "7",
title: "Title 8",
},
]
export function Test() {
const [cards, setCards] = useState(data)
const [removed, setRemoved] = useState([])
function removeItem(id) {
let previous = [...cards]
let itemToRemove = previous.find((x) => x.id === id)
setCards(previous.filter((c) => c.id !== id))
setRemoved([...removed, itemToRemove])
}
return (
<SafeAreaView style={{ margin: 20 }}>
<FlatList
data={cards}
numColumns={2}
keyExtractor={(item) => item.id}
renderItem={({ index, item }) => (
<SwipeRow swipeGestureEnded={() => removeItem(item.id)}>
<View />
<View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
<Text>{item.title}</Text>
</View>
</SwipeRow>
)}
/>
</SafeAreaView>
)
}
Notice that we need some kind of property in our objects in order to determine which one we want to remove. I have used a basic id
property here, which is quite common using FlatList
. If you are retrieving your data from an API
which does not provide the same id
, then we could just do some preprocessing (normalization
) first and add the id
prop ourselves.
The initial view looks as follows.

Swiping, let's say the item with 'Title 6' to the right or to the left removes it.
It might be desired to implement the following feature as well.
- If the item is in the first column, then only swiping to the left will remove the item.
- If the item is in the second column, then only swiping to the right will remove the item.
This is easily implemented using the index
param which is passed to the renderItem
function and the vx
prop of the gestureState
passed to the swipeGestureEnded
function.
Here is fully working implementation.
import React, { useState } from "react"
import { FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"
const data = [
{
id: "0",
title: "Title 1",
},
{
id: "1",
title: "Title 2",
},
{
id: "2",
title: "Title 3",
},
{
id: "3",
title: "Title 4",
},
{
id: "4",
title: "Title 5",
},
{
id: "5",
title: "Title 6",
},
{
id: "6",
title: "Title 7",
},
{
id: "7",
title: "Title 8",
},
]
export function Test() {
const [cards, setCards] = useState(data)
const [removed, setRemoved] = useState([])
function removeItem(id) {
let previous = [...cards]
let itemToRemove = previous.find((x) => x.id === id)
setCards(previous.filter((c) => c.id !== id))
setRemoved([...removed, itemToRemove])
}
return (
<SafeAreaView style={{ margin: 20 }}>
<FlatList
data={cards}
numColumns={2}
keyExtractor={(item) => item.id}
renderItem={({ index, item }) => (
<SwipeRow
swipeGestureEnded={(key, event) => {
if (event.gestureState.vx < 0) {
if (index % 2 === 0) {
removeItem(item.id)
}
} else if (event.gestureState.vx >= 0) {
if (index % 2 === 1) {
removeItem(item.id)
}
}
}}
disableLeftSwipe={index % 2 === 1}
disableRightSwipe={index % 2 === 0}>
<View />
<View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
<Text>{item.title}</Text>
</View>
</SwipeRow>
)}
/>
</SafeAreaView>
)
}
Since the index is zero based in a FlatList
, an item is in the second column if and only if index % 2 === 1
(e.g. an item with index 3
is always in the second column and thus not divisible by 2
), on the other hand an item is in the first column if and only if index % 2 === 0
that is index
is divisible by 2
.
There are several callback function props in the SwipeRowComponent
that should be fired in certain situations. However, most of them did not work in my setup and I still have no clue why. I got it to work by using the event.gestureState.vx
property which is negative if we swipe to the left and positive (including zero) if we swipe to the right.
It might be desired to implement an undo
button as it is quite common in this kind of functionalities. This can be done as follows:
- Implement a second state which represents a
Queue
that holds lastly removed items. The undo
button then just pops
the lastly removed item.
Here is a fully working implementation with a dummy undo
button that achieves exactly that.
import React, { useState } from "react"
import { Button, FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"
const data = [
{
id: "0",
title: "Title 1",
},
{
id: "1",
title: "Title 2",
},
{
id: "2",
title: "Title 3",
},
{
id: "3",
title: "Title 4",
},
{
id: "4",
title: "Title 5",
},
{
id: "5",
title: "Title 6",
},
{
id: "6",
title: "Title 7",
},
{
id: "7",
title: "Title 8",
},
]
export function Test() {
const [cards, setCards] = useState(data)
const [removed, setRemoved] = useState([])
function removeItem(id) {
let previous = [...cards]
let itemToRemove = previous.find((x) => x.id === id)
setCards(previous.filter((c) => c.id !== id))
setRemoved([...removed, itemToRemove])
}
function undoRemove() {
if (removed && removed.length > 0) {
let itemToUndo = removed[removed.length - 1]
setCards([...cards, itemToUndo])
setRemoved(removed.filter((c) => c.id !== itemToUndo.id))
}
}
return (
<SafeAreaView style={{ margin: 20 }}>
<FlatList
data={cards}
numColumns={2}
keyExtractor={(item) => item.id}
renderItem={({ index, item }) => (
<SwipeRow
swipeGestureEnded={(key, event) => {
if (event.gestureState.vx < 0) {
if (index % 2 === 0) {
removeItem(item.id)
}
} else if (event.gestureState.vx >= 0) {
if (index % 2 === 1) {
removeItem(item.id)
}
}
}}
disableLeftSwipe={index % 2 === 1}
disableRightSwipe={index % 2 === 0}>
<View />
<View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
<Text>{item.title}</Text>
</View>
</SwipeRow>
)}
/>
<Button onPress={undoRemove} title="Undo" />
</SafeAreaView>
)
}
Notice that my undo
button just appends the removed item to the end of the list. If you want to keep the initial index, then you need to save the old index and push the item to the correct position.
Here is workin snack of my last implementation.