Skip to content

Instantly share code, notes, and snippets.

@okaybeydanol
Created February 21, 2025 21:57
Show Gist options
  • Save okaybeydanol/dba5e46e8981622e6d0d2b07205e928c to your computer and use it in GitHub Desktop.
Save okaybeydanol/dba5e46e8981622e6d0d2b07205e928c to your computer and use it in GitHub Desktop.
RN: Swipeable Item with Reanimated 3 - Lightweight & Performant List Item with Swipe Actions
import React, {
forwardRef,
PropsWithChildren,
useImperativeHandle,
memo,
useMemo,
useCallback,
} from 'react';
import {
Gesture,
GestureDetector,
PanGesture,
} from 'react-native-gesture-handler';
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
} from 'react-native-reanimated';
import {StyleSheet, View} from 'react-native';
export interface SwipeableHandle {
closeSwipeable: () => void;
}
interface SwipeableProps extends PropsWithChildren {
width: number;
height?: number;
onSwipeableOpen?: () => void;
renderRightItem?: () => React.ReactNode;
}
interface SwipeableConfig {
MIN_POSITION: number;
MAX_POSITION: number;
VELOCITY_THRESHOLD: number;
}
interface RightItemMemoProps {
width: number;
height?: number;
renderRightItem?: () => React.ReactNode;
}
interface AnimatedContentProps {
children?: React.ReactNode;
panHandlers: PanGesture;
animatedStyle: {
transform: {
translateX: number;
}[];
};
}
const TIMING_CONFIG = {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
};
const RightItemMemo = memo(
({width, height, renderRightItem}: RightItemMemoProps) => {
const rightItemStyle = useMemo(
() => [styles.rightItem, {width, height}],
[width, height],
);
return <View style={rightItemStyle}>{renderRightItem?.()}</View>;
},
() => true,
);
const AnimatedContentMemo = memo(
({children, panHandlers, animatedStyle}: AnimatedContentProps) => (
<GestureDetector gesture={panHandlers}>
<Animated.View style={animatedStyle}>{children}</Animated.View>
</GestureDetector>
),
() => true,
);
const Swipeable = forwardRef<SwipeableHandle, SwipeableProps>(
({children, onSwipeableOpen, width, height, renderRightItem}, ref) => {
const Swipeable_CONFIG = useMemo<SwipeableConfig>(
() => ({
MIN_POSITION: -width,
MAX_POSITION: 0,
VELOCITY_THRESHOLD: 0.1,
}),
[width],
);
const onSwipeableOpenMemo = useCallback(() => {
onSwipeableOpen?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useImperativeHandle(ref, () => ({
closeSwipeable: () => {
translateX.value = withTiming(Swipeable_CONFIG.MAX_POSITION, {
...TIMING_CONFIG,
});
},
}));
const startX = useSharedValue(0);
const translateX = useSharedValue(0);
const handlePositionChange = useCallback((translationX: number) => {
'worklet';
const newPosition = startX.value + translationX;
translateX.value = Math.min(
Swipeable_CONFIG.MAX_POSITION,
Math.max(Swipeable_CONFIG.MIN_POSITION, newPosition),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleGestureEnd = useCallback(
(velocityX: number) => {
'worklet';
const currentPosition = translateX.value;
const halfMinPosition = Swipeable_CONFIG.MIN_POSITION / 2;
const isHighVelocity =
Math.abs(velocityX) > Swipeable_CONFIG.VELOCITY_THRESHOLD;
const targetPosition = isHighVelocity
? velocityX < 0
? Swipeable_CONFIG.MIN_POSITION
: Swipeable_CONFIG.MAX_POSITION
: currentPosition < halfMinPosition
? Swipeable_CONFIG.MIN_POSITION
: Swipeable_CONFIG.MAX_POSITION;
translateX.value = withTiming(targetPosition, {
...TIMING_CONFIG,
duration: isHighVelocity ? 200 : 300,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const panGesture = Gesture.Pan()
.onStart(() => {
startX.value = translateX.value;
if (onSwipeableOpen) {
runOnJS(onSwipeableOpenMemo)();
}
})
.onChange(({translationX}) => {
handlePositionChange(translationX);
})
.onEnd(({velocityX}) => {
handleGestureEnd(velocityX);
})
.activeOffsetX([-20, 20]);
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{translateX: translateX.value}],
}),
[],
);
return (
<View style={styles.container}>
{renderRightItem && (
<RightItemMemo
width={width}
height={height}
renderRightItem={renderRightItem}
/>
)}
<AnimatedContentMemo
panHandlers={panGesture}
animatedStyle={animatedStyle}>
{children}
</AnimatedContentMemo>
</View>
);
},
);
const styles = StyleSheet.create({
container: {
position: 'relative',
backfaceVisibility: 'hidden',
shadowColor: 'transparent',
shadowOpacity: 0,
elevation: 0,
},
rightItem: {
position: 'absolute',
right: 0,
zIndex: -1,
overflow: 'hidden',
backfaceVisibility: 'hidden',
shadowColor: 'transparent',
shadowOpacity: 0,
elevation: 0,
},
});
Swipeable.displayName = 'Swipeable';
RightItemMemo.displayName = 'RightItem';
AnimatedContentMemo.displayName = 'AnimatedContent';
export default memo(Swipeable, () => true);
@okaybeydanol
Copy link
Author

A performant React Native swipeable item component optimized for large lists (10k+ items). Uses Reanimated 3 and Gesture Handler for smooth animations with gesture-driven animations running on the UI thread. Perfect for implementing swipe-to-action functionality in lists with delete/archive actions.

Key Features:

  • ๐Ÿš€ Optimized for large lists
  • ๐Ÿ’จ UI thread animations
  • ๐ŸŽฏ Zero bridge communication
  • ๐Ÿ”„ Worklet-based calculations
  • ๐Ÿ›  Customizable right action component
  • ๐Ÿ“ฑ Hardware accelerated animations
  • ๐Ÿงฎ Smart velocity-based animations
  • ๐Ÿ”‹ Highly efficient animations

Usage:

<Swipeable
  width={164}
  height={64}
  renderRightItem={() => <DeleteButton />}
  onSwipeableOpen={() => console.log('Opened')}
  ref={ref => setSwipeableRef(ref?.closeSwipeable)}>
  <YourListItem />
</Swipeable>

Dependencies:

  • react-native-reanimated: ^3.x
  • react-native-gesture-handler: ^2.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment