Gestures with Gesture Handler in React Native: Swipe-to-Delete
Gestures with Gesture Handler: Swipe-to-Delete
~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
React Native's built-in PanResponder system for gesture recognition runs entirely on the JS thread – with more complex gestures (multiple simultaneous touches, swipe thresholds), this can cause noticeable stuttering. react-native-gesture-handler instead recognizes gestures NATIVELY, combined with Reanimated from the last chapter for a fully JS-thread-independent swipe-to-delete feature in our cart.
Installation
npx expo install react-native-gesture-handler// App.js, at the very top, BEFORE all other imports:
import 'react-native-gesture-handler';Achtung: The 'react-native-gesture-handler' import MUST be the VERY FIRST line in App.js – EXACTLY the same rule as with the drawer navigator from the "React Native Reference" series (the "Drawer Navigation" topic), since both build on the same native setup.
First: creating a real CartScreen
So far our app doesn't display the cart's CONTENTS anywhere, only the badge from chapter 1/2. Let's build a full CartScreen with swipe-to-delete:
import { View, Text, StyleSheet } from 'react-native';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
} from 'react-native-reanimated';
import { useCartStore } from '../store/cartStore';
const SWIPE_THRESHOLD = -100;
function SwipeableCartItem({ item, onRemove }) {
const translateX = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = Math.min(0, event.translationX);
})
.onEnd(() => {
if (translateX.value < SWIPE_THRESHOLD) {
translateX.value = withTiming(-500, {}, () => {
runOnJS(onRemove)(item.sku);
});
} else {
translateX.value = withTiming(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.item, animatedStyle]}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemPrice}>${item.price.toFixed(2)}</Text>
</Animated.View>
</GestureDetector>
);
}
function CartScreen() {
const cart = useCartStore((state) => state.cart);
const removeProduct = useCartStore((state) => state.removeProduct);
if (cart.length === 0) {
return <Text style={styles.emptyText}>Your cart is empty.</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.hint}>← Swipe left to remove</Text>
{cart.map((item) => (
<SwipeableCartItem key={item.sku} item={item} onRemove={removeProduct} />
))}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 16, paddingHorizontal: 16 },
hint: { textAlign: 'center', color: '#9ca3af', marginBottom: 12, fontSize: 12 },
item: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#f9fafb',
padding: 14,
borderRadius: 8,
marginBottom: 8,
},
itemName: { fontSize: 16 },
itemPrice: { fontSize: 16, fontWeight: 'bold' },
emptyText: { textAlign: 'center', marginTop: 40, color: '#9ca3af' },
});
export default CartScreen;Adding removeProduct to cartStore.js (if not already there)
Chapter 2 only showed cartStore.js with addProduct – for CartScreen we add removeProduct, following the same pattern as favoritesSlice in chapter 3:
// In store/cartStore.js, inside the persist() object, add after addProduct:
removeProduct(sku) {
set({ cart: get().cart.filter((item) => item.sku !== sku) });
},The new building blocks in detail
Gesture.Pan()creates a gesture recognizer for drag movements –.onUpdatefires on EVERY movement (with the currentevent.translationXdistance since the gesture began),.onEndon release.Math.min(0, event.translationX)clamps the movement to NEGATIVE values only (leftward) – a swipe to the right has no effect.runOnJS(onRemove)(item.sku)is critical:onUpdate/onEndcallbacks are WORKLETS, running on the UI thread – the actualremoveProductcall (which updates the Zustand store, JS-thread code) MUST explicitly be sent back to the JS thread viarunOnJS(). A direct call toonRemove(item.sku)inside the worklet would throw a runtime error.GestureDetectorinstead of the olderPanGestureHandlercomponent pattern – the new, recommended API generation of Gesture Handler, built on the same worklet concept as Reanimated.
Achtung: runOnJS is one of the most common pitfalls when combining Reanimated and Gesture Handler: EVERY call to "normal" JavaScript functions (state updates, store dispatches, navigation) INSIDE a worklet must be wrapped with runOnJS(). Only pure calculations and assignments to .value are allowed to happen DIRECTLY inside the worklet.
Wiring CartScreen into navigation
Add another <Stack.Screen> entry for CartScreen to App.js (analogous to FavoritesScreen from chapter 3), and link the cart badge from ProductListScreen (chapter 1) to it, instead of just displaying it.
Tipp: Test on a REAL device or in the simulator with mouse drag – you'll feel the swipe motion follow your finger/cursor WITHOUT delay, even if you deliberately block the JS thread as a test (see the filterItemsSlowly technique from "React for Professionals" chapter 33) – a direct, tangible proof of what JSI-based worklets make possible.