Skip to main content

Mobile Swipe Actions

This note documents a recurring React Native list issue: a swipe action is visible, but pressing the action does nothing.

Symptom​

  • The list row can be swiped and the action button is visible.
  • Screen-level logs inside renderLeftActions or renderRightActions appear.
  • Logs inside the action button onPress do not appear.
  • The issue can look like a permission or business-rule problem, but the press event never reaches the action button.

Root cause​

SwipeableFlashList renders action buttons behind the card and moves the card with translateX.

If the translated card content still has a higher touch layer than the action area, the action can be visible while the card layer still receives the tap. In this repo, itemContent already has zIndex: 1, so action layers must be explicitly raised above it.

Fix pattern​

In apps/mobile/src/components/SwipeableFlashList/index.tsx:

  • set pointerEvents="box-none" on the action wrapper and inner action-content wrapper
  • keep the action wrapper at pointerEvents="none" until the row is actually open; otherwise invisible actions can be pressed before swiping
  • give both leftActions and rightActions a higher zIndex and elevation than itemContent
  • keep the action Pressable enabled when debug logs are needed, even if no business action should run

Expected action-layer shape:

<Animated.View
pointerEvents={actionsTouchable ? 'box-none' : 'none'}
style={[styles.leftActions, leftActionStyle]}
>
<View pointerEvents="box-none" style={styles.actionContent}>
{leftActions}
</View>
</Animated.View>

Set actionsTouchable to true only when the pan gesture decides to open the row, and set it back to false on close/reset. This prevents invisible action buttons from receiving taps while still allowing visible buttons to be pressed after a swipe.

Expected styles:

leftActions: {
...StyleSheet.absoluteFill,
zIndex: 2,
elevation: 2,
},
rightActions: {
...StyleSheet.absoluteFill,
zIndex: 2,
elevation: 2,
},
itemContent: {
width: '100%',
zIndex: 1,
},

Debug checklist​

  1. Log inside renderLeftActions and renderRightActions.
  2. Log inside the action Pressable onPress.
  3. If render logs appear but press logs do not, inspect the swipe action layer, not the permission logic.
  4. Confirm swipe direction:
    • leftActions opens when the row is dragged to the right.
    • rightActions opens when the row is dragged to the left.
  5. If a disabled-looking action still needs debug logging, keep the Pressable enabled and call config.onPress?.() conditionally.

Invoice example​

invoiceListScreen uses this pattern to debug cancel actions. The useful logs are:

  • [InvoiceListScreen] cancel swipe render
  • [InvoiceListScreen] swipe action press
  • [InvoiceListScreen] cancel swipe press

When checking owner permissions, log both:

  • loginUserCode
  • invoiceUserCode
  • rawInvoiceUserCode

Normalize numeric user codes before comparing because API data can omit leading zeroes. For example, 12265 and 012265 must compare as the same user.

Worklet closure safety​

Repeated [Worklets] Cannot copy value of type FiberNode warnings occur when swipe callbacks capture the leftActions / rightActions React elements. The warning path traverses props, children, and _owner; the resulting log volume is consistent with the reported debugger V8 heap exhaustion.

Both apps/mobile/src/components/SwipeableFlashList/index.tsx and apps/pos-terminal/src/components/SwipeableRow.tsx compute hasLeftActions = Boolean(leftActions) and hasRightActions = Boolean(rightActions) on the JS thread. onUpdate and onDeactivate capture those booleans for action-presence checks; React elements stay in JS rendering.

When merging branches that independently fixed this issue, keep each flag declaration only once. Git can merge cleanly while TypeScript detects duplicate declarations.

apps/mobile/__tests__/swipeableWorklets.test.tsx checks actual Babel-generated closures for React elements and swipe behavior on both apps, across six cases. Reload the app and reopen DevTools for device verification of warnings and memory behavior. Hiding warnings or increasing the heap does not address the closure bug.

yarn mobile:test --watchman=false --runInBand swipeableWorklets.test.tsx
yarn typecheck