Skip to content

Instantly share code, notes, and snippets.

@okaybeydanol
Created February 21, 2025 21:49
Show Gist options
  • Save okaybeydanol/8d3a19636cd2048240328bd97baba7e7 to your computer and use it in GitHub Desktop.
Save okaybeydanol/8d3a19636cd2048240328bd97baba7e7 to your computer and use it in GitHub Desktop.
RN: Swipeable Item with RN Animated - Lightweight & Performant List Item with Swipe Actions
import React, {
PropsWithChildren,
useRef,
useImperativeHandle,
forwardRef,
memo,
useCallback,
useMemo,
useEffect,
} from 'react';
import {
View,
PanResponder,
StyleSheet,
Easing,
Animated,
GestureResponderHandlers,
GestureResponderEvent,
} 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;
}
type AnimatedValueWithValue = Animated.Value & {
_value: number;
};
interface RightItemMemoProps {
width: number;
height?: number;
renderRightItem?: () => React.ReactNode;
}
interface AnimatedContentProps {
children?: React.ReactNode;
panHandlers: GestureResponderHandlers;
animatedStyle: {
transform: {
translateX: Animated.AnimatedInterpolation<string | 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) => (
<Animated.View {...panHandlers} style={animatedStyle}>
{children}
</Animated.View>
),
() => 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: () => {
Animated.timing(pan, {
toValue: Swipeable_CONFIG.MAX_POSITION,
...TIMING_CONFIG,
useNativeDriver: true,
}).start();
},
}));
useEffect(() => {
return () => pan.stopAnimation();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const pan = useRef(
new Animated.Value(Swipeable_CONFIG.MAX_POSITION, {
useNativeDriver: true,
}),
).current as AnimatedValueWithValue;
const onMoveShouldSetPanResponder = useCallback(
(_: GestureResponderEvent, {dx}: {dx: number}) => {
return Math.abs(dx) > 20;
},
[],
);
const onPanResponderRelease = useCallback(
(_: GestureResponderEvent, {vx}: {vx: number}) => {
pan.flattenOffset();
const velocityX = vx;
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
: pan._value < halfMinPosition
? Swipeable_CONFIG.MIN_POSITION
: Swipeable_CONFIG.MAX_POSITION;
Animated.timing(pan, {
toValue: targetPosition,
...TIMING_CONFIG,
duration: isHighVelocity ? 200 : 300,
useNativeDriver: true,
}).start();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder,
onPanResponderGrant: () => {
pan.extractOffset();
onSwipeableOpenMemo();
},
onPanResponderMove: Animated.event([null, {dx: pan}], {
useNativeDriver: false,
}),
onPanResponderRelease,
onPanResponderTerminate: onPanResponderRelease,
}),
).current;
const animatedStyle = useMemo(
() => ({
transform: [
{
translateX: pan.interpolate({
inputRange: [
Swipeable_CONFIG.MIN_POSITION,
Swipeable_CONFIG.MAX_POSITION,
],
outputRange: [
Swipeable_CONFIG.MIN_POSITION,
Swipeable_CONFIG.MAX_POSITION,
],
extrapolate: 'clamp',
}),
},
],
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
return (
<View style={styles.container}>
{renderRightItem && (
<RightItemMemo
width={width}
height={height}
renderRightItem={renderRightItem}
/>
)}
<AnimatedContentMemo
panHandlers={panResponder.panHandlers}
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: 0,
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 React Native's Animated API for smooth animations with minimal JS bridge usage. Perfect for implementing swipe-to-action functionality in lists with delete/archive actions.

Key Features:

  • ๐Ÿš€ Optimized for large lists
  • ๐Ÿ’จ Native driver animations
  • ๐ŸŽฏ Minimal JS bridge usage
  • ๐Ÿ”„ Smooth gesture handling
  • ๐Ÿ›  Customizable right action component
  • ๐Ÿ“ฑ Hardware accelerated animations
  • ๐Ÿงฎ Smart velocity-based animations
  • ๐Ÿ”‹ Battery efficient

Usage:

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

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