I am using React Native Swipeable for "swipe to select" functionality in the mobile app I am working on. I have a list of invoices. Each invoice is swipeable. When user swipe right, it displays selected checkbox in the left menu and invoice number should be added to selectedInvoices state of the invoices list component, without having to take any further action. To achieve this I used onSwipeableOpen and all works as expected, apart one thing. When user swipe right, invoice number is being added to the array of selected invoices, but swipeable component is getting closed straight away, so user cannot see left menu that indicates it is selected. Any idea how to prevent swipeable auto closing after executing onSwipeableOpen?
Link to the original package documentation: https://docs.swmansion.com/react-native-gesture-handler/docs/api/components/swipeable/
Current Behaviour
Swipeable Document closes automatically after onSwipeableOpen execution when invoice number is being added to selectedInvoices state (array) in InvoicesList component.
Expected Behaviour
Swipeable Document stays open, to display left menu to indicate invoice has been selected.
InvoicesList.tsx component
import Modal from 'react-native-modal';
import Moment from 'moment';
import { ColorSchemeName, StyleSheet, Text, TextStyle, useColorScheme, View, ViewStyle } from 'react-native';
import { Colours } from '../../assets/styles';
import { Document } from '../UI';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { InvoiceType } from '../../types';
import { RootState } from '../../redux/store';
import { SCREEN_HEIGHT, SCREEN_WIDTH, WINDOW_FONT_SCALE } from '../../services/DimensionsHelper';
import { useSelector } from 'react-redux';
import { useState } from 'react';
type Props = {
invoices: Array<InvoiceType>,
showSwipeDemo: boolean,
swipeDemoContentStyle: ViewStyle,
containerStyle?: ViewStyle,
};
export default function InvoicesList({ invoices, showSwipeDemo, swipeDemoContentStyle, containerStyle }: Props): JSX.Element {
const theme: Style = getStyle(useColorScheme());
const swipeDemoSeen: boolean = useSelector((state: RootState) => state.settings.swipeDemoSeen);
const firstInvoice: InvoiceType = invoices[0];
const [selectedInvoices, setSelectedInvoices] = useState<Array<number>>([]);
const selectInvoice = (invoiceNumber: number): void => {
setSelectedInvoices((currentSelectedInvoices: Array<number>) => [...currentSelectedInvoices, invoiceNumber]);
};
const unselectInvoice = (invoiceNumber: number): void => {
setSelectedInvoices((currentSelectedInvoices: Array<number>) => currentSelectedInvoices.filter((invoice: number) => invoice !== invoiceNumber));
};
return (
<>
<GestureHandlerRootView>
<View style={[ theme.container, containerStyle ]}>
{
invoices.map((invoice: InvoiceType, index: number) =>
<Document
key={ index }
number={ invoice.glitem }
title={ 'Invoice #' + invoice.documentNumber.toString() }
status={ invoice.status }
description={ invoice.customerReference }
date={ 'Invoiced ' + Moment(invoice.documentDate).format('D MMM YYYY ') }
value={ invoice.value }
onSwipeRight={ selectInvoice }
onSwipeableClose={ unselectInvoice }
showSwipeDemo={ false }
/>
)
}
</View>
</GestureHandlerRootView>
<Modal
customBackdrop={ <View style={ theme.modalBackdrop } /> }
backdropOpacity={ 0.7 }
statusBarTranslucent={ true }
isVisible={ showSwipeDemo && !swipeDemoSeen }
animationIn='fadeIn'
animationOut='fadeOut'
useNativeDriver={ true }
useNativeDriverForBackdrop={ true }
hideModalContentWhileAnimating={ true }
>
<View style={[ theme.modalContent, swipeDemoContentStyle ]}>
<GestureHandlerRootView>
<Document
number={ firstInvoice.glitem }
title={ 'Invoice #' + firstInvoice.documentNumber.toString() }
status={ firstInvoice.status }
description={ firstInvoice.customerReference }
date={ 'Invoiced ' + Moment(firstInvoice.documentDate).format('D MMM YYYY ') }
value={ firstInvoice.value }
onSwipeRight={ () => {} }
onSwipeableClose={ () => {} }
showSwipeDemo={ showSwipeDemo && !swipeDemoSeen }
/>
</GestureHandlerRootView>
<View style={ theme.instructionContainer }><Text style={ theme.instruction }>Swipe right to select</Text></View>
</View>
</Modal>
</>
);
}
type Style = {
container: ViewStyle,
modalBackdrop: ViewStyle,
modalContent: ViewStyle,
instructionContainer: ViewStyle,
instruction: TextStyle,
};
const getStyle = (colourScheme: ColorSchemeName): Style => {
return StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingBottom: 32,
},
modalBackdrop: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
backgroundColor: Colours.black.s100,
},
modalContent: {
position: 'absolute',
width: '101%',
marginLeft: -2,
},
instructionContainer: {
alignItems: 'center',
},
instruction: {
fontFamily: 'Montserrat_700Bold',
fontSize: 18 / WINDOW_FONT_SCALE,
lineHeight: 22 / WINDOW_FONT_SCALE,
color: Colours.white.s100,
textTransform: 'uppercase',
},
});
};
Document.tsx component
import MkmIcon from './MkmIcon';
import StatusLabel from './StatusLabel';
import { ColorSchemeName, Pressable, StyleSheet, Text, TextStyle, useColorScheme, View, ViewStyle } from 'react-native';
import { Colours } from '../../assets/styles';
import { Swipeable } from 'react-native-gesture-handler';
import { WINDOW_FONT_SCALE } from '../../services/DimensionsHelper';
import { setSwipeDemoSeen } from '../../redux/settingsSlice';
import { useDispatch } from 'react-redux';
import { useLayoutEffect, useRef } from 'react';
type Props = {
number: number,
title: string,
status: string,
description?: string,
date: string,
value: number,
onPress?: (number: number) => void,
onSwipeRight?: (number: number) => void,
onSwipeableClose?: (number: number) => void,
showSwipeDemo?: boolean,
};
export default function Document({ number, title, status, description, date, value, onPress, onSwipeRight, onSwipeableClose, showSwipeDemo }: Props): JSX.Element {
const theme: Style = getStyle(useColorScheme());
const swipeableRef = useRef(null);
const dispatch = useDispatch();
const onPressHandler = (): void => {
if (onPress) {
onPress(number);
}
};
const onSwipeRightHandler = (): void => {
if (onSwipeRight) {
onSwipeRight(number);
}
};
const onSwipeableCloseHandler = (): void => {
if (onSwipeableClose) {
onSwipeableClose(number);
}
};
const renderLeftCheckBox = (progress): JSX.Element => {
return (
<View style={ theme.checkContainer }>
<MkmIcon name='CheckThick' colour={ Colours.white.s100 } style={ theme.check } />
</View>
);
};
const Container = ({ children }): JSX.Element => {
return (
onSwipeRight && onSwipeableClose
? <Swipeable
ref={ swipeableRef }
containerStyle={ theme.swipeableContainer }
renderLeftActions={ renderLeftCheckBox }
onSwipeableOpen={ onSwipeRightHandler }
onSwipeableClose={ onSwipeableCloseHandler }
overshootLeft={ false }
>{ children }</Swipeable>
: <View style={ theme.container }>{ children }</View>
);
};
useLayoutEffect(() => {
if (showSwipeDemo) {
const swipeable: Swipeable = swipeableRef.current;
setTimeout(() => {
swipeable.openLeft();
}, 1000);
setTimeout(() => {
swipeable.close();
}, 2500);
setTimeout(() => {
dispatch(setSwipeDemoSeen());
}, 3500)
}
}, [showSwipeDemo]);
return (
<Container>
<Pressable style={ ({ pressed }) => [theme.documentContainer, (onPress && pressed) && theme.pressed ] } onPress={ onPressHandler }>
<View style={ theme.row }>
<View style={ theme.column }>
<Text style={ theme.title }>{ title }</Text>
</View>
<View>
<StatusLabel status={ status } />
</View>
</View>
{
description &&
<View style={ theme.descriptionContainer }>
<Text style={ theme.description }>{ description }</Text>
</View>
}
<View style={[ theme.row, theme.rowBottom ]}>
<View style={ theme.column }>
<Text style={ theme.date }>{ date }</Text>
</View>
<View>
<Text style={ theme.amount }>£{ value }</Text>
</View>
</View>
</Pressable>
</Container>
);
}
type Style = {
container: ViewStyle,
swipeableContainer: ViewStyle,
documentContainer: ViewStyle,
pressed: ViewStyle,
row: ViewStyle,
column: ViewStyle,
title: TextStyle,
descriptionContainer: ViewStyle,
description: TextStyle,
rowBottom: ViewStyle,
date: TextStyle,
amount: TextStyle,
checkContainer: ViewStyle,
check: ViewStyle,
};
const getStyle = (colourScheme: ColorSchemeName): Style => {
return StyleSheet.create({
container: {
marginBottom: 16,
},
swipeableContainer: {
padding: -1,
marginBottom: 16,
borderRadius: 4,
backgroundColor: Colours.success.s100,
},
documentContainer: {
padding: 16,
borderWidth: 1,
borderBottomWidth: 4,
borderRadius: 4,
borderColor: Colours.grey.s30,
backgroundColor: Colours.white.s100,
},
pressed: {
opacity: 0.6,
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
alignContent: 'space-between',
},
column: {
flex: 1,
},
title: {
fontFamily: 'Montserrat_500Medium',
fontSize: 14 / WINDOW_FONT_SCALE,
lineHeight: 21 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
descriptionContainer: {
paddingTop: 8,
},
description: {
fontFamily: 'Montserrat_600SemiBold',
fontSize: 16 / WINDOW_FONT_SCALE,
lineHeight: 24 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
rowBottom: {
paddingTop: 12,
},
date: {
fontFamily: 'Montserrat_500Medium',
fontSize: 14 / WINDOW_FONT_SCALE,
lineHeight: 21 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s60,
},
amount: {
fontFamily: 'Montserrat_700Bold',
fontSize: 16 / WINDOW_FONT_SCALE,
lineHeight: 24 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
checkContainer: {
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
},
check: {
width: 30,
height: 30,
padding: 7,
borderWidth: 1,
borderColor: Colours.white.s100,
borderRadius: 15,
},
});
};

After small break to refresh my brain and then couple of hours further digging I found the root cause and the solution.
Swipeable component actually has not been getting closed after
onSwipeableOpenexecution. It looked like it's been getting closed as the component was re-rendered after updatingselectedInvoicesstate.It's because I've been using
useStatehook, then updating state is always causing re-render.Solution was to use
useRefhook to storeselectedInvoicesbecauseuseRefdoes not cause component re-render.