Skip to content

[General] Hide non-serializable gesture fields from enumeration - #4556

Merged
m-bert merged 1 commit into
mainfrom
@mbert/enumerable-properties
Oct 2, 2026
Merged

m-bert merged 1 commit into
mainfrom
@mbert/enumerable-properties

Conversation

@m-bert

@m-bert m-bert commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Description

When a gesture object is captured by a worklet, for example onActivate: () => GestureStateManager.fail(pan.handlerTag) inside a workletized callback, Worklets serializes the whole object with Object.entries and, in dev, freezes every enumerated field. That dragged config (all callbacks and shared values), detectorCallbacks and gestureRelations to the UI runtime and left the JS-side objects frozen. The freeze turned the gestureRelations reassignment in traverseAndConfigureRelations into a silent no-op and pushing into the frozen relation arrays throws once they are non-empty.

First, the following warning appears:

 WARN  [Worklets] Cannot copy value of type `WorkletEventHandler`. It was located at `<worklet handleEvent>["__closure"]["eventHandler"]<worklet reactNativeReanimated_useComposedEventHandlerCommonTs1>["__closure"]["workletsMap"]["onGestureHandlerReanimatedEvent"][1]<worklet useReanimatedEventHandlerTs3>["__closure"]["workletizedHandlers"]["onActivate"]<worklet indexTsx1>["__closure"]["pan"]["detectorCallbacks"]["reanimatedEventHandler"]["workletEventHandler"]`

Then after trying to modify frozen relations:

 WARN  [Worklets] Tried to modify key `gestureRelations` of an object which has been already passed to a worklet. See
        https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-serializable
        for more details.
 ERROR  [TypeError: cannot add a new property]

The only field that needs to cross to the UI runtime is handlerTag. This PR marks type, config, detectorCallbacks and gestureRelations as non-enumerable in useGesture. All consumers read them by name or with the in operator, so library behaviour is unchanged; only enumeration-based cloning, spreading and console.log now see { handlerTag }.

Composed gestures returned by useComposedGesture are left as they are.

Test plan

Tested on the following code:
import React, { useMemo, useReducer } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import {
  GestureDetector,
  GestureHandlerRootView,
  GestureStateManager,
  usePanGesture,
  useSimultaneousGestures,
  useTapGesture,
} from 'react-native-gesture-handler';

export default function EmptyExample() {
  const [renders, rerender] = useReducer((x: number) => x + 1, 1);

  // Stable config keeps the same `pan` object across renders
  // (React Compiler does this for an inline literal as well).
  const panConfig = useMemo(() => ({}), []);
  const pan = usePanGesture(panConfig);

  // Inline literal, so babel workletizes `onActivate`. The worklet captures `pan`,
  // Worklets serializes the whole gesture object and freezes it in dev.
  const tap = useTapGesture({
    onActivate: () => {
      GestureStateManager.fail(pan.handlerTag);
    },
  });

  // Composed gesture is a new object every render, so relations get re-traversed
  // on every render and `pan.gestureRelations` is reassigned on the frozen object.
  const composed = useSimultaneousGestures(pan, tap);

  return (
    <GestureHandlerRootView style={styles.container}>
      <Text>Renders: {renders}</Text>
      <Button title="Re-render" onPress={rerender} />
      <GestureDetector gesture={composed}>
        <View style={styles.box} />
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    gap: 16,
  },
  box: {
    width: 160,
    height: 160,
    borderRadius: 12,
    backgroundColor: '#3b82f6',
  },
});

Copilot AI balanced review requested due to automatic review settings October 1, 2026 10:34
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 3c479962-a684-4ba7-b431-4362992af327

📥 Commits

Reviewing files that changed from the base of the PR and between 3f01ff6 and 55591e0.

📒 Files selected for processing (1)
  • packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Refactor
    • Updated how gesture settings and callbacks are exposed internally. Gesture behavior and configuration remain unchanged, so there are no noticeable changes to the end-user experience.

Walkthrough

useGesture now creates a local gesture object in its memoized callback and marks four properties as non-enumerable. The gesture values and memoization dependencies remain unchanged.

Changes

Gesture properties

Layer / File(s) Summary
Gesture object properties
packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
The memoized callback creates the gesture object and marks type, config, detectorCallbacks, and gestureRelations as non-enumerable. The dependency list remains unchanged.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 55591

The change hides gesture internals from enumeration while preserving direct access and composition behavior. No merge-blocking issue is established; merge after normal checks.

Architecture Summary

Architecture risk: 🔵 Low · up to 55591

The change affects 1 system.

Changed systems: packages/react-native-gesture-handler

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — packages/react-native-gesture-handler (library) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts: useGesture now creates a local gesture object inside the memoized callback instead of returning an object literal directly.
  • observed — Modified behavior in packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts: The memoized callback marks type, config, detectorCallbacks, and gestureRelations as non-enumerable before returning the gesture object. Its dependency list is unchanged.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: hiding non-serializable gesture fields from enumeration.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new serialization contract needs an automated regression test.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Prevents Worklets from serializing and freezing internal v3 gesture fields.

Changes:

  • Makes internal gesture fields non-enumerable.
  • Keeps handlerTag enumerable and named property access unchanged.
File Description
packages/​react-native-gesture-handler/​src/​v3/​hooks/​useGesture.ts Restricts gesture enumeration to handlerTag.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +76 to +80
Object.defineProperties(g, {
type: { enumerable: false },
config: { enumerable: false },
detectorCallbacks: { enumerable: false },
gestureRelations: { enumerable: false },
@m-bert
m-bert requested a review from j-piasecki October 1, 2026 11:36
@m-bert
m-bert merged commit f936e8b into main Oct 2, 2026
12 checks passed
@m-bert
m-bert deleted the @mbert/enumerable-properties branch October 2, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants