In React Native Gesture Handler 3, how do you build swipe-to-delete episode rows with ReanimatedSwipeable and keep only one row open?
answer
- import from its own subpath
- renderRightActions gets progress and translation
- rightThreshold defaults to half the panel
- friction and overshootFriction
- ref methods: close, openLeft, openRight, reset
basics
~20 sWrap each row in ReanimatedSwipeable and render the delete button with renderRightActions, animating it from the progress and translation shared values. Keep a ref to the open row and call close() on it when another row starts opening.
solid answer
~40 s`ReanimatedSwipeable`, imported from `react-native-gesture-handler/ReanimatedSwipeable`, needs Reanimated installed and the app inside `GestureHandlerRootView`. It puts the row in a horizontally pannable container; `renderRightActions(progress, translation, methods)` renders what is revealed on a left swipe, where `progress` is a shared value from `0` (closed) to `1` (open) that keeps growing on overshoot, so the action animates in `useAnimatedStyle` without re-rendering. Tune feel with `friction` (default `1`), `rightThreshold` (default half the actions panel), `overshootRight` and `overshootFriction` (docs suggest 8 or more for a native feel). Callbacks such as `onSwipeableWillOpen` and `onSwipeableOpen` receive the direction and run on the JS side, so state updates are fine there. For one open row at a time, keep a ref to the currently open row's `SwipeableMethods` and call `close()` on it in the next row's `onSwipeableWillOpen`.
code
tsx · 62 linesimport { useRef } from 'react';
import { FlatList, Pressable, StyleSheet, Text } from 'react-native';
import ReanimatedSwipeable, { type SwipeableMethods } from 'react-native-gesture-handler/ReanimatedSwipeable';
import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
type Episode = { id: string; title: string };
function DeleteAction({ progress, onDelete }: { progress: SharedValue<number>; onDelete: () => void }) {
const style = useAnimatedStyle(() => ({
opacity: Math.min(progress.value, 1),
transform: [{ scale: 0.8 + 0.2 * Math.min(progress.value, 1) }],
}));
return (
<Animated.View style={[styles.action, style]}>
<Pressable accessibilityRole="button" onPress={onDelete}>
<Text style={styles.actionText}>Delete</Text>
</Pressable>
</Animated.View>
);
}
export function Downloads({ episodes, onDelete }: { episodes: Episode[]; onDelete: (id: string) => void }) {
const openRow = useRef<SwipeableMethods | null>(null);
const rowRefs = useRef(new Map<string, SwipeableMethods>());
return (
<FlatList
data={episodes}
keyExtractor={(e) => e.id}
renderItem={({ item }) => (
<ReanimatedSwipeable
ref={(r) => {
if (r) rowRefs.current.set(item.id, r);
else rowRefs.current.delete(item.id);
}}
friction={2}
rightThreshold={40}
overshootFriction={8}
onSwipeableWillOpen={() => {
const me = rowRefs.current.get(item.id) ?? null;
if (openRow.current && openRow.current !== me) openRow.current.close();
openRow.current = me;
}}
onSwipeableClose={() => {
if (openRow.current === rowRefs.current.get(item.id)) openRow.current = null;
}}
renderRightActions={(progress) => (
<DeleteAction progress={progress} onDelete={() => onDelete(item.id)} />
)}
>
<Text style={styles.row}>{item.title}</Text>
</ReanimatedSwipeable>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16, backgroundColor: 'white' },
action: { width: 88, justifyContent: 'center', alignItems: 'center', backgroundColor: 'red' },
actionText: { color: 'white' },
});go deeper
Recall the import path, that the app needs GestureHandlerRootView, and that renderRightActions renders what a left swipe reveals.
Explain the progress and translation shared values, the threshold and friction props with their defaults, and the ref methods close, openLeft, openRight and reset.
Coordinate rows so one stays open, keep vertical scrolling reliable with drag offsets, animate actions on the UI thread, and plan an accessible alternative to the swipe.
Decide whether destructive actions belong behind a swipe at all, and set a consistent swipe vocabulary across the app's lists.
## What `ReanimatedSwipeable` is `ReanimatedSwipeable` is Gesture Handler's component for swipeable rows: the list item slides sideways to reveal actions underneath, like delete or download on an episode in a podcast app's downloads list. It is the drop-in replacement for the older `Swipeable` component, rewritten on Reanimated, and in Gesture Handler 3 it is built on the new hook API. Prerequisites: - Reanimated (and therefore `react-native-worklets`) installed. - The app wrapped in `GestureHandlerRootView`. - Import from its own subpath: `import ReanimatedSwipeable, { type SwipeableMethods } from 'react-native-gesture-handler/ReanimatedSwipeable'`. ## Rendering the actions Two render props define what is revealed: - `renderLeftActions` for a swipe to the right, - `renderRightActions` for a swipe to the left. Each receives three arguments: 1. **`progress`**: a Reanimated shared value, `0` when closed and `1` when fully open, approaching infinity as the row overshoots. 2. **`translation`**: a shared value with the row's horizontal offset from its closed position. 3. **`swipeableMethods`**: the same methods the ref exposes. Because `progress` and `translation` are shared values, the action button can scale, fade or slide in `useAnimatedStyle` on the UI thread while the finger moves, with no React renders. Render them in a small component so hooks are used legally. ## Tuning the feel | Prop | Default | Effect | |---|---|---| | `friction` | `1` | How much the panel lags the finger; `2` moves at half speed | | `rightThreshold` / `leftThreshold` | Half the actions panel's width | Release distance past which it opens (or closes) | | `overshootRight` / `overshootLeft` | `true` when that side's render function exists | Whether the row can be pulled past the actions | | `overshootFriction` | `1` (no friction) | Drag resistance while overshooting; docs suggest `8` or more | | `dragOffsetFromLeft` / `dragOffsetFromRight` | `10` / `-10` | Horizontal travel needed before a swipe activates | The drag offsets set how far the finger must travel horizontally (10 points either way by default) before the swipe activates, which leaves a mostly vertical drag to the list's scroll. In Gesture Handler 3, `dragOffsetFromRight` takes a negative value, and several props (`enabled`, `hitSlop`, the drag offsets) also accept shared values. ## Callbacks and threads The lifecycle callbacks receive the swipe direction: - `onSwipeableOpenStartDrag`, `onSwipeableCloseStartDrag` when a drag begins; - `onSwipeableWillOpen`, `onSwipeableWillClose` when the open or close animation starts; - `onSwipeableOpen`, `onSwipeableClose` when it finishes. The component dispatches them to the JS side, so calling `setState` or a delete mutation from them is fine. ## Keeping one row open Users expect that opening a second row closes the first. `ReanimatedSwipeable` does not coordinate rows by itself, so: 1. Give each row a ref typed `SwipeableMethods`. 2. Keep a ref (not state) holding the currently open row's methods at list level. 3. In a row's `onSwipeableWillOpen`, call `close()` on the stored row if it is a different one, then store this row. 4. Clear the stored row in `onSwipeableClose`. The ref methods are: - `close()` animates the row shut; - `openLeft()` and `openRight()` open it programmatically; - `reset()` clears the swiping state **without** animation, which is useful when a row component is reused for different data. ## Deleting safely When the delete action fires, remove the item from your data and let the row unmount; keying the list by episode id ensures the next row does not inherit the open state. If you animate the removal, Reanimated's `exiting` and `layout` props on the row's container belong to the layout-animation toolkit rather than the swipeable. ## What it does not cover Swipes are invisible to screen readers, so every swipe action also needs an accessible alternative, such as a custom accessibility action or a visible button. Coordinating a swipeable with other gestures uses its `simultaneousWith`, `requireToFail` and `block` props, which follow Gesture Handler's composition rules.
- Why doesn't a vertical scroll of the episode list accidentally open ReanimatedSwipeable rows?The swipe activates only after the finger travels `dragOffsetFromLeft` (default `10`) or `dragOffsetFromRight` (default `-10`) horizontally from the start point. A mostly vertical drag is usually taken by the list's scroll before that happens. If rows still open during scrolling, raise those offsets.
- What is the difference between close() and reset() on a ReanimatedSwipeable ref?`close()` animates the row back to its closed position and fires the close callbacks' lifecycle. `reset()` clears the swiping state immediately without any animation, which suits a row being reused for different data, where an animated close would be visible and wrong.
saying these in an interview costs you the question
- ReanimatedSwipeable works without GestureHandlerRootView at the app root.
- Only one row can be open at a time by default, with no extra code.
- The progress value stops at 1, so overshoot cannot be animated.
- onSwipeableOpen runs on the UI thread, so setState inside it throws.
- dragOffsetFromRight takes a positive value in Gesture Handler 3.
- A swipe action needs no alternative for screen reader users.