Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:

screens/CartScreen.js
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 – .onUpdate fires on EVERY movement (with the current event.translationX distance since the gesture began), .onEnd on 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/onEnd callbacks are WORKLETS, running on the UI thread – the actual removeProduct call (which updates the Zustand store, JS-thread code) MUST explicitly be sent back to the JS thread via runOnJS(). A direct call to onRemove(item.sku) inside the worklet would throw a runtime error.
  • GestureDetector instead of the older PanGestureHandler component 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.