Implement Pressable based on Touchable - #4411
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughSummary by CodeRabbit
Walkthrough
ChangesPressable interaction handling
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Pressable
participant PressableWithTouchable
participant Touchable
Caller->>Pressable: provide Pressable props
Pressable->>PressableWithTouchable: select default implementation
PressableWithTouchable->>Touchable: configure mapped props and callbacks
Touchable-->>PressableWithTouchable: emit press or hover event
PressableWithTouchable-->>Caller: dispatch Pressable callback
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx (1)
55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRename the component to
StatefulPressable.The component is named
Pressablebut the file isStatefulPressable.tsx, andPressable.tsxexports a different component with the same name. React DevTools, error boundaries, and stack traces then show two distinct components asPressable.♻️ Proposed rename
-const Pressable = (props: PressableProps) => { +const StatefulPressable = (props: PressableProps) => {Update the export at line 453:
-export default Pressable; +export default StatefulPressable;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx` at line 55, Rename the component declaration currently named Pressable to StatefulPressable and update its corresponding export so the module consistently exposes that name. Preserve all existing props, behavior, and references within the component.packages/react-native-gesture-handler/src/v3/components/Pressable.tsx (1)
20-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a
__DEV__warning when relation props appear after mount.The engine choice is latched at mount. If a consumer adds
simultaneousWith,requireToFail, orblocklater,PressableWithTouchablesilently drops them (seePressableWithTouchable.tsxlines 84-91). The press keeps working, so the failure is invisible and hard to debug.A development-only warning makes the constraint explicit without changing runtime behavior.
♻️ Proposed dev-only warning
const Pressable = (props: PressableProps) => { const usesRelations = useRef( props.simultaneousWith != null || props.requireToFail != null || props.block != null ).current; + if (__DEV__ && !usesRelations) { + if ( + props.simultaneousWith != null || + props.requireToFail != null || + props.block != null + ) { + console.warn( + '[RNGH] Pressable: relation props added after mount are ignored. Pass them on the first render.' + ); + } + } + return usesRelations ? (🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/v3/components/Pressable.tsx` around lines 20 - 31, In the Pressable component, add a __DEV__-only warning when relation props (simultaneousWith, requireToFail, or block) become defined after the initial mount while usesRelations remains false. Preserve the existing latched engine selection and runtime behavior, and warn that these props are ignored by PressableWithTouchable.packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx (1)
260-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBoth responder assertions check a constant return value.
Touchable.onStartShouldSetResponderCapturealways returnsfalse, so both tests pass regardless ofkeyboardShouldPersistTapsmode and cannot detect a regression in the responder-claim side effect.
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L260-L266: replace the return-value check with an assertion that the Pressable claims the responder event for RNGH inhandledmode.packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L285-L290: assert that no responder claim reaches the ScrollView outsidehandledmode, instead of re-checking the samefalsereturn value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx` around lines 260 - 266, Update the responder assertions in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx at lines 260-266 and 285-290: in the handled-mode test, assert that the Pressable claims the responder event for RNGH rather than checking Touchable.onStartShouldSetResponderCapture’s constant false return; outside handled mode, assert that no responder claim reaches the ScrollView. Use the existing responder-claim tracking or mock symbols in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx`:
- Around line 139-148: Update handlePressIn to clear any existing
timers.current.press before scheduling a new press-delay timeout, while
preserving the immediate firePressIn path when no delay is configured.
- Line 217: Update the delayLongPress prop in PressableWithTouchable so an
undefined value defaults to 500 ms before being passed to Touchable, matching
StatefulPressable’s default while preserving explicitly provided delays.
In
`@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx`:
- Around line 86-99: Mirror the timer cleanup used in PressableWithTouchable.tsx
by adding an unmount cleanup effect in StatefulPressable that clears
longPressTimeoutRef, pressDelayTimeoutRef, hoverInTimeout, and hoverOutTimeout.
Ensure pending callbacks cannot invoke onLongPress, onPressIn, onHoverIn, or
onHoverOut after unmount.
---
Nitpick comments:
In `@packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx`:
- Around line 260-266: Update the responder assertions in
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx at lines
260-266 and 285-290: in the handled-mode test, assert that the Pressable claims
the responder event for RNGH rather than checking
Touchable.onStartShouldSetResponderCapture’s constant false return; outside
handled mode, assert that no responder claim reaches the ScrollView. Use the
existing responder-claim tracking or mock symbols in the test.
In `@packages/react-native-gesture-handler/src/v3/components/Pressable.tsx`:
- Around line 20-31: In the Pressable component, add a __DEV__-only warning when
relation props (simultaneousWith, requireToFail, or block) become defined after
the initial mount while usesRelations remains false. Preserve the existing
latched engine selection and runtime behavior, and warn that these props are
ignored by PressableWithTouchable.
In
`@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx`:
- Line 55: Rename the component declaration currently named Pressable to
StatefulPressable and update its corresponding export so the module consistently
exposes that name. Preserve all existing props, behavior, and references within
the component.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f9731866-b7a1-40e4-8365-a9bb7fbd9c8f
📒 Files selected for processing (4)
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsxpackages/react-native-gesture-handler/src/v3/components/Pressable.tsxpackages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsxpackages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx
| const longPressTimeoutRef = useRef<number | null>(null); | ||
| const pressDelayTimeoutRef = useRef<number | null>(null); | ||
| const isOnPressAllowed = useRef<boolean>(true); | ||
| const jsResponderContext = use(JSResponderContext); | ||
| const isCurrentlyPressed = useRef<boolean>(false); | ||
| const dimensions = useRef<PressableDimensions>({ | ||
| width: 0, | ||
| height: 0, | ||
| }); | ||
|
|
||
| // When the touch that begins a press is the one dismissing the keyboard | ||
| // (keyboardShouldPersistTaps="never"), the press is swallowed to match RN's | ||
| // touchables. | ||
| const dropKeyboardTapRef = useRef<boolean | null>(null); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Pending timers are never cleared on unmount.
longPressTimeoutRef, pressDelayTimeoutRef, hoverInTimeout, and hoverOutTimeout hold scheduled callbacks. The file contains no cleanup effect. If the component unmounts while a timer is pending, onLongPress, onPressIn, onHoverIn, or onHoverOut still fires and calls into a torn-down component.
PressableWithTouchable.tsx lines 99-116 add exactly this cleanup. Mirror it here so both engines behave the same.
🐛 Proposed cleanup effect
const hoverInTimeout = useRef<number | null>(null);
const hoverOutTimeout = useRef<number | null>(null);
+
+ useEffect(
+ () => () => {
+ for (const timer of [
+ longPressTimeoutRef,
+ pressDelayTimeoutRef,
+ hoverInTimeout,
+ hoverOutTimeout,
+ ]) {
+ if (timer.current) {
+ clearTimeout(timer.current);
+ timer.current = null;
+ }
+ }
+ },
+ []
+ );Also applies to: 234-235
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx`
around lines 86 - 99, Mirror the timer cleanup used in
PressableWithTouchable.tsx by adding an unmount cleanup effect in
StatefulPressable that clears longPressTimeoutRef, pressDelayTimeoutRef,
hoverInTimeout, and hoverOutTimeout. Ensure pending callbacks cannot invoke
onLongPress, onPressIn, onHoverIn, or onHoverOut after unmount.
There was a problem hiding this comment.
This will be fixed in a follow-up
There was a problem hiding this comment.
@m-bert, acknowledged. The timer cleanup remains pending for the follow-up.
Would you like me to open a GitHub issue for it?
You are interacting with an AI system.
There was a problem hiding this comment.
Pull request overview
This PR refactors the v3 Pressable implementation by splitting it into two internal engines while keeping the public API unchanged: a default engine built on the native-button Touchable, and a state-machine-based engine used when gesture relation props are provided.
Changes:
- Added
PressableWithTouchable(default engine) and extracted the prior state-machine logic intoStatefulPressable. - Updated
Pressableto choose the engine once at mount time based on presence of relation props (simultaneousWith/requireToFail/block). - Updated v3 API tests to assert responder handling against the rendered pressable button rather than the prior NativeDetector path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx | New extracted state-machine-based Pressable engine (used when relation props are present). |
| packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx | New Touchable-based Pressable engine intended as the default path. |
| packages/react-native-gesture-handler/src/v3/components/Pressable.tsx | Wrapper now routes to the appropriate internal engine once per mount. |
| packages/react-native-gesture-handler/src/tests/api_v3.test.tsx | Adjusts ScrollView responder tests to target the pressable button node. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@coderabbitai review again pls 👀 |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx (1)
61-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider dropping the relation props in one destructuring step.
restis destructured a second time only to removecancelable,dimensionsAfterResize,simultaneousWith,requireToFail, andblock. You can list these keys in the first destructuring and delete theeslint-disableblock.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx` around lines 61 - 99, Update the initial props destructuring in PressableWithTouchable to omit cancelable, dimensionsAfterResize, simultaneousWith, requireToFail, and block alongside the other excluded props, then use the resulting rest object directly and remove the redundant second destructuring and eslint suppression.packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx (1)
260-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe new responder assertions cannot fail in either keyboard mode.
onStartShouldSetResponderCaptureinTouchablereturnsfalseunconditionally; its real effect is theupdateResponderEventValue(jsResponderContext, true)call, which neither test observes. Both new assertions therefore pass regardless of the mode under test.
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L260-L266: replace the constant-return assertion with an observable outcome, for example that the keyboard stays open and thePressablecallbacks run.packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L285-L290: remove the redundant return-value assertion, or assert that the press does dismiss the keyboard in this mode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx` around lines 260 - 266, The responder assertions do not verify mode-specific behavior because onStartShouldSetResponderCapture always returns false. In packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L260-L266, replace the return-value checks with observable outcomes such as the keyboard remaining open and Pressable callbacks running; in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L285-L290, remove the redundant assertion or verify that pressing dismisses the keyboard in that mode.packages/react-native-gesture-handler/src/v3/components/Pressable.tsx (1)
20-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWarn in development when relation props appear after mount.
The engine choice is latched at mount. If a consumer renders
Pressablewithout relation props and later passessimultaneousWith,requireToFail, orblock,PressableWithTouchabledestructures those props and discards them. The gesture relation is then silently inactive for the whole lifetime of the component. A__DEV__warning makes this contract visible.♻️ Proposed dev-only warning
const Pressable = (props: PressableProps) => { + const hasRelations = + props.simultaneousWith != null || + props.requireToFail != null || + props.block != null; + const usesRelations = useRef( - props.simultaneousWith != null || - props.requireToFail != null || - props.block != null + hasRelations ).current; + if (__DEV__ && hasRelations !== usesRelations) { + console.warn( + '[RNGH] Pressable: `simultaneousWith`/`requireToFail`/`block` were added or removed after mount. The implementation is selected once at mount, so the change has no effect.' + ); + } + return usesRelations ? (🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-native-gesture-handler/src/v3/components/Pressable.tsx` around lines 20 - 31, Add a development-only warning in the Pressable component when relation props are introduced after mount, using a ref to compare the current presence of simultaneousWith, requireToFail, and block against the initial usesRelations value. Keep the existing latched renderer selection unchanged and warn only when the initial selection was PressableWithTouchable but a relation prop later becomes present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx`:
- Around line 199-208: Align Touchable long-press timing with StatefulPressable
by including unstable_pressDelay in the delayLongPress value and flushing the
pending press timer before invoking the long-press handler. Update the relevant
press-timer and long-press handling around makeActiveHandler so onLongPress
cannot fire before the deferred onPressIn.
In
`@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx`:
- Line 84: Update StatefulPressable’s pressed-state handling so style and
function-valued children derive their displayed state from the current
testOnly_pressed prop when provided, while retaining pressedState otherwise;
apply testOnly_pressed ?? pressedState at both callback invocation sites and add
rerender coverage for prop changes.
---
Nitpick comments:
In `@packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx`:
- Around line 260-266: The responder assertions do not verify mode-specific
behavior because onStartShouldSetResponderCapture always returns false. In
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L260-L266,
replace the return-value checks with observable outcomes such as the keyboard
remaining open and Pressable callbacks running; in
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx#L285-L290,
remove the redundant assertion or verify that pressing dismisses the keyboard in
that mode.
In `@packages/react-native-gesture-handler/src/v3/components/Pressable.tsx`:
- Around line 20-31: Add a development-only warning in the Pressable component
when relation props are introduced after mount, using a ref to compare the
current presence of simultaneousWith, requireToFail, and block against the
initial usesRelations value. Keep the existing latched renderer selection
unchanged and warn only when the initial selection was PressableWithTouchable
but a relation prop later becomes present.
In
`@packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx`:
- Around line 61-99: Update the initial props destructuring in
PressableWithTouchable to omit cancelable, dimensionsAfterResize,
simultaneousWith, requireToFail, and block alongside the other excluded props,
then use the resulting rest object directly and remove the redundant second
destructuring and eslint suppression.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 62bf5133-b2d8-43e9-86dd-c90524816683
📒 Files selected for processing (4)
packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsxpackages/react-native-gesture-handler/src/v3/components/Pressable.tsxpackages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsxpackages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx
| ...remainingProps | ||
| } = props; | ||
|
|
||
| const [pressedState, setPressedState] = useState(testOnly_pressed ?? false); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file='packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx'
ast-grep outline "$file" --lang tsx
printf '\n--- relevant source ---\n'
sed -n '1,130p' "$file"
sed -n '380,425p' "$file"
printf '\n--- prop and state usages ---\n'
rg -n -C 3 'testOnly_pressed|pressedState|style|children' "$file"
printf '\n--- related tests ---\n'
rg -n -C 3 'StatefulPressable|testOnly_pressed' packages/react-native-gesture-handler --glob '*.{ts,tsx,js,jsx}'Repository: software-mansion/react-native-gesture-handler
Length of output: 14287
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- StatefulPressable state transitions and cleanup ---'
sed -n '80,245p' packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx
sed -n '245,405p' packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx
printf '%s\n' '--- PressableProps contract ---'
sed -n '160,195p' packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx
printf '%s\n' '--- comparable implementations ---'
sed -n '75,125p' packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx
sed -n '70,115p' packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx
printf '%s\n' '--- testOnly_pressed references ---'
rg -n -C 5 'testOnly_pressed' . --glob '!node_modules/**' --glob '!build/**' --glob '!dist/**'Repository: software-mansion/react-native-gesture-handler
Length of output: 25090
🏁 Script executed:
#!/bin/bash
set -e
node - <<'JS'
function mount(testOnly_pressed) {
let pressedState = testOnly_pressed ?? false;
return {
render(nextTestOnlyPressed) {
// Current implementation: useState initializer is ignored after mount.
const currentImplementation = pressedState;
// Proposed implementation: testOnly_pressed controls only the displayed value.
const proposedImplementation = nextTestOnlyPressed ?? pressedState;
return { currentImplementation, proposedImplementation };
},
pressIn() {
pressedState = true;
},
pressOut() {
pressedState = false;
},
};
}
const cases = [
['false -> true', false, true],
['true -> false', true, false],
['true -> removed', true, undefined],
['false -> removed', false, undefined],
];
for (const [name, initial, next] of cases) {
const state = mount(initial);
console.log(name, state.render(initial));
console.log(' rerender', state.render(next));
}
const interaction = mount(undefined);
interaction.pressIn();
console.log('interaction press-in with override removed', interaction.render(undefined));
interaction.pressOut();
console.log('interaction press-out with override removed', interaction.render(undefined));
JS
printf '\n--- local Pressable documentation and implementation references ---\n'
rg -n -C 8 'testOnly_pressed|currently pressed|style.*function|children.*function' \
packages/react-native-gesture-handler/src \
packages/docs-gesture-handler/docs \
packages/docs-gesture-handler/versioned_docsRepository: software-mansion/react-native-gesture-handler
Length of output: 40709
🌐 Web query:
React Native Pressable source testOnly_pressed useState style pressed children prop updates
💡 Result:
The testOnly_pressed prop in React Native's Pressable component is a specialized property intended exclusively for documentation, snapshot testing, or automated testing scenarios [1][2]. It allows developers to force the component into its pressed state programmatically, bypassing the need for manual press events [3]. When you set testOnly_pressed={true} on a Pressable component, it overrides the internal state and effectively behaves as if the component is being pressed [3]. This triggers the Pressable component to execute its style and children functions with the pressed property set to true [3][4]. For example, if your Pressable uses a function for its style or children prop, passing testOnly_pressed will force those functions to receive { pressed: true } as an argument [3][4]: <Pressable testOnly_pressed={true} style={({ pressed }) => ({ backgroundColor: pressed? 'red': 'blue' })} > {({ pressed }) => {pressed? 'Active': 'Idle'}} In this example, both the style and the children render function would reflect the 'pressed' state, rendering the background red and the text as 'Active' [4]. This is particularly useful for verifying the UI appearance of the pressed state in testing environments without requiring simulated user interactions like firing press events [3]. Note that this prop should be used only for testing and debugging purposes [1][2].
Citations:
- 1: https://reactnative.dev/docs/pressable
- 2: https://cdn.jsdelivr.net/npm/react-native@0.80.2/Libraries/Components/Pressable/Pressable.d.ts
- 3: https://stackoverflow.com/questions/64452964/testing-the-pressed-state-of-a-pressable
- 4: https://github.com/facebook/react-native-website/blob/main/docs/pressable.md
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import urllib.request
urls = [
"https://raw.githubusercontent.com/facebook/react-native/main/packages/react-native/Libraries/Components/Pressable/Pressable.js",
"https://raw.githubusercontent.com/facebook/react-native/v0.80.2/packages/react-native/Libraries/Components/Pressable/Pressable.js",
]
for url in urls:
print(f"--- {url} ---")
try:
text = urllib.request.urlopen(url, timeout=10).read().decode()
except Exception as exc:
print(f"ERROR: {exc}")
continue
lines = text.splitlines()
for i, line in enumerate(lines):
if "testOnly_pressed" in line or "pressed:" in line:
start = max(0, i - 8)
end = min(len(lines), i + 12)
print("\n".join(f"{j+1}: {lines[j]}" for j in range(start, end)))
print()
PYRepository: software-mansion/react-native-gesture-handler
Length of output: 740
Derive the displayed pressed state from testOnly_pressed.
useState(testOnly_pressed ?? false) reads testOnly_pressed only on mount. Prop changes can leave style and function-valued children with a stale pressed state. Use testOnly_pressed ?? pressedState when invoking both callbacks. Add rerender coverage for prop changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx`
at line 84, Update StatefulPressable’s pressed-state handling so style and
function-valued children derive their displayed state from the current
testOnly_pressed prop when provided, while retaining pressedState otherwise;
apply testOnly_pressed ?? pressedState at both callback invocation sites and add
rerender coverage for prop changes.
Source: Coding guidelines
There was a problem hiding this comment.
Changes to old Pressable will be considered in a follow-up
There was a problem hiding this comment.
@m-bert, understood. This finding affects the extracted state-machine-based Pressable and is out of scope for this PR. I will leave it for the follow-up.
Would you like me to create a GitHub issue for the testOnly_pressed state update and rerender coverage?
You are interacting with an AI system.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx:183
- The deferred
onPressIntimer doesn't guard againstdisabledflipping before it fires. Even with timer cleanup on unmount, a disabled toggle can still result inonPressInfiring later unless the callback checks current disabled state (or is cleared synchronously on disable).
timers.current.press = setTimeout(() => {
timers.current.press = null;
firePressIn(event);
}, unstable_pressDelay);
packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx:453
- After renaming the component to
StatefulPressable, the default export should be updated to export the renamed symbol (otherwise the file will no longer compile).
export default Pressable;
packages/react-native-gesture-handler/src/v3/components/PressableWithTouchable.tsx:124
unstable_pressDelay/hover timers can still fire after the component becomes disabled (e.g.disabledtoggles to true while a delayedonPressInis pending), causing callbacks/state updates on a now-disabled Pressable. Consider tracking the latest disabled state and clearing pending timers + resetting pressed state whendisabledbecomes true.
This issue also appears on line 180 of the same file.
const [pressed, setPressed] = useState(testOnly_pressed ?? false);
const timers = useRef<Timers>({ press: null, hoverIn: null, hoverOut: null });
const dimensions = useRef<PressableDimensions>({ width: 0, height: 0 });
// Whether the in-progress press activated within hitSlop (see handlePressIn).
const isActive = useRef(false);
useEffect(
() => () => {
const pending = timers.current;
if (pending.press) {
clearTimeout(pending.press);
}
if (pending.hoverIn) {
clearTimeout(pending.hoverIn);
}
if (pending.hoverOut) {
clearTimeout(pending.hoverOut);
}
},
[]
);
packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx:55
- The component is declared as
PressableinStatefulPressable.tsx, which makes stack traces/devtools ambiguous next to the publicPressablewrapper. Renaming the component to match the file improves debugging and avoids confusion.
This issue also appears on line 453 of the same file.
const Pressable = (props: PressableProps) => {
| const usesRelations = useRef( | ||
| props.simultaneousWith != null || | ||
| props.requireToFail != null || | ||
| props.block != null | ||
| ).current; |
There was a problem hiding this comment.
Reading refs in render is an antipattern. I don't think it bites here since the value doesn't change, but still. I think we can allow swapping the two at runtime, with its consequences.
Description
This PR splits
Pressableinto two components - currentPressablewith state machine and newPressablebased onTouchable. Public API remains unchanged - the split is internal.Pressablebased onTouchableis chosen by default. Old implementation is used only when relation properties are passed.Test plan
Added an example under new_api > Tests > "Pressable engines (Touchable vs Stateful)" that renders both engines side by side with identical props and logs the callback order. Toggle each prop and confirm both columns behave the same.
Example code