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

Gesten mit Gesture Handler in React Native: Swipe-to-Delete

Gesten mit Gesture Handler: Swipe-to-Delete

~17 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026

React Natives eingebautes PanResponder-System zur Gestenerkennung läuft vollständig auf dem JS-Thread – bei komplexeren Gesten (mehrere gleichzeitige Touches, Swipe-Schwellenwerte) kann das zu spürbarem Ruckeln führen. react-native-gesture-handler erkennt Gesten stattdessen NATIV, kombiniert mit Reanimated aus dem letzten Kapitel für eine vollständig JS-Thread-unabhängige Swipe-to-Delete-Funktion in unserem Warenkorb.

Installation

npx expo install react-native-gesture-handler
// App.js, ganz oben, VOR allen anderen Imports:
import 'react-native-gesture-handler';

Achtung: Der Import 'react-native-gesture-handler' MUSS die ALLERERSTE Zeile in App.js sein – GENAU dieselbe Regel wie beim Drawer-Navigator aus der "React Native Referenz"-Serie (Thema "Drawer Navigation"), da beide auf demselben nativen Setup aufbauen.

Zuerst: einen echten CartScreen anlegen

Bisher zeigt unsere App den Warenkorb-INHALT nirgends an, nur das Badge aus Kapitel 1/2. Wir bauen jetzt einen vollständigen CartScreen mit 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}>Ihr Warenkorb ist leer.</Text>;
  }

  return (
    <View style={styles.container}>
      <Text style={styles.hint}>← Nach links wischen zum Entfernen</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;

cartStore.js um removeProduct ergänzen (falls noch nicht vorhanden)

Kapitel 2 zeigte cartStore.js nur mit addProduct – für CartScreen ergänzen wir removeProduct analog zu favoritesSlices Muster aus Kapitel 3:

// In store/cartStore.js, innerhalb des persist()-Objekts, nach addProduct ergänzen:
removeProduct(sku) {
  set({ cart: get().cart.filter((item) => item.sku !== sku) });
},

Die neuen Bausteine im Detail

  • Gesture.Pan() erzeugt einen Gesten-Erkenner für Ziehbewegungen – .onUpdate feuert bei JEDER Bewegung (mit der aktuellen event.translationX-Distanz seit Gestenbeginn), .onEnd beim Loslassen.
  • Math.min(0, event.translationX) begrenzt die Bewegung auf NEGATIVE Werte (nur nach links) – ein Wisch nach rechts hat keinen Effekt.
  • runOnJS(onRemove)(item.sku) ist entscheidend: onUpdate/onEnd-Callbacks sind WORKLETS, laufen auf dem UI-Thread – der eigentliche removeProduct-Aufruf (der den Redux-artigen Zustand-Store aktualisiert, JS-Thread-Code) MUSS explizit mit runOnJS() zurück auf den JS-Thread geschickt werden. Ein direkter Aufruf von onRemove(item.sku) innerhalb des Worklets würde einen Laufzeitfehler auslösen.
  • GestureDetector statt des älteren PanGestureHandler-Komponenten-Musters – die neue, empfohlene API-Generation von Gesture Handler, aufbauend auf demselben Worklet-Konzept wie Reanimated.

Achtung: runOnJS ist eine der häufigsten Stolperfallen beim Kombinieren von Reanimated und Gesture Handler: JEDER Aufruf "normaler" JavaScript-Funktionen (State-Updates, Store-Dispatches, Navigation) INNERHALB eines Worklets muss durch runOnJS() gekapselt werden. Nur reine Berechnungen und Zuweisungen an .value dürfen DIREKT im Worklet passieren.

CartScreen in die Navigation einbinden

Ergänzen Sie App.js um einen weiteren <Stack.Screen>-Eintrag für CartScreen (analog zu FavoritesScreen aus Kapitel 3) und verlinken Sie das Warenkorb-Badge aus ProductListScreen (Kapitel 1) darauf, statt es nur anzuzeigen.

Tipp: Testen Sie auf einem ECHTEN Gerät oder im Simulator mit Maus-Drag – Sie werden spüren, dass die Wisch-Bewegung dem Finger/Cursor OHNE Verzögerung folgt, selbst wenn Sie testweise den JS-Thread künstlich blockieren (siehe die filterItemsSlowly-Technik aus "React für Profis" Kapitel 33) – ein direkter, spürbarer Beweis für das, was JSI-basierte Worklets ermöglichen.