Skip to content

Commit a83496b

Browse files
authored
Keep Reanimated deep imports out of web bundles (#4620)
## Description #4554 gates `Wrap.getAnimatableRef()` on whether Reanimated re-resolves the animatable ref on update. The check deep-imports `react-native-reanimated/src/createAnimatedComponent/AnimatedComponent` and looks for `_refreshComponentRef` on its prototype. It lives in `reanimatedWrapper.ts`, which every platform loads. Reanimated has no `exports` map. Its `react-native` field points at `src/`, but `main` and `module` point at `lib/module`. Bundlers that ignore the `react-native` field (Vite, webpack) therefore resolve the package root to `lib/module` and the deep import to `src/`, which is a second copy of Reanimated. In Vite dev the optimizer leaves the `.tsx` deep import as a raw import, and `src/platform-specific/workletsVersion.ts` default-imports a CommonJS script, so the module graph fails and the app never mounts. In production the bundle contains every Reanimated module twice. Metro, including Expo web, keeps `react-native` first and is unaffected. The check moves to `GestureDetector/refreshesAnimatableRef.ts`, imported only by the native `Wrap.tsx`. `Wrap.web.tsx` already uses the plain `Wrap`, so web bundles no longer reach the deep import. Native behaviour is unchanged. Fixes #4619. ## Test plan - Standalone Vite 8 + react-native-web app with one `GestureDetector`, RNGH 3.4.0: dev server fails with `validate-worklets-version.js does not provide an export named 'default'`, production bundle 919 kB with 169 Reanimated modules bundled from both `lib/module` and `src/`. - Same app with this branch built into `node_modules`: dev server renders and the tap gesture works, production bundle 781 kB with 0 modules from `src/`. - `yarn test src/__tests__/legacyWrapRef.test.tsx`, `yarn ts-check`, `yarn lint:js` pass.
1 parent 2202d7b commit a83496b

4 files changed

Lines changed: 30 additions & 29 deletions

File tree

‎packages/react-native-gesture-handler/src/__tests__/legacyWrapRef.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { cleanup, render } from '@testing-library/react-native';
22
import React from 'react';
33
import { findNodeHandle, View } from 'react-native';
44

5+
import * as refreshesAnimatableRef from '../handlers/gestures/GestureDetector/refreshesAnimatableRef';
56
import { Wrap } from '../handlers/gestures/GestureDetector/Wrap';
6-
import * as reanimatedWrapper from '../handlers/gestures/reanimatedWrapper';
77
import { Gesture, GestureDetector, GestureHandlerRootView } from '../index';
88
import RNGestureHandlerModule from '../RNGestureHandlerModule';
99

@@ -139,7 +139,7 @@ describe('Legacy GestureDetector ref forwarding', () => {
139139

140140
beforeEach(() => {
141141
refreshSpy = jest.spyOn(
142-
reanimatedWrapper,
142+
refreshesAnimatableRef,
143143
'reanimatedRefreshesAnimatableRef'
144144
);
145145
});

‎packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/Wrap.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import {
88
resolveHostInstance,
99
} from '../../../hostInstance';
1010
import { tagMessage } from '../../../utils';
11-
import {
12-
Reanimated,
13-
reanimatedRefreshesAnimatableRef,
14-
} from '../reanimatedWrapper';
11+
import { Reanimated } from '../reanimatedWrapper';
12+
import { reanimatedRefreshesAnimatableRef } from './refreshesAnimatableRef';
1513

1614
export class Wrap extends React.Component<{
1715
onGestureHandlerEvent?: unknown;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Reanimated resolves `getAnimatableRef()` once, when its ref to the wrapped
2+
// component attaches. Only versions that re-resolve it on update (their
3+
// `AnimatedComponent` has `_refreshComponentRef`) can be handed a child's host
4+
// instance that may change, as the legacy `Wrap` does. The class is not
5+
// exported, so probe it by subpath, as the v3 detector does for
6+
// `NativeEventsManager`.
7+
//
8+
// Only the native `Wrap` imports this module. The subpath resolves to a second
9+
// copy of Reanimated under bundlers that ignore its `react-native` field
10+
// (Vite, webpack), so it must stay out of web bundles.
11+
let refreshesAnimatableRef = false;
12+
try {
13+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
14+
const AnimatedComponent =
15+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
16+
require('react-native-reanimated/src/createAnimatedComponent/AnimatedComponent').default;
17+
refreshesAnimatableRef =
18+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19+
typeof AnimatedComponent?.prototype?._refreshComponentRef === 'function';
20+
} catch {
21+
// Reanimated is missing or older; keep handing Reanimated the Wrap itself.
22+
}
23+
24+
export function reanimatedRefreshesAnimatableRef() {
25+
return refreshesAnimatableRef;
26+
}

‎packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts‎

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -132,27 +132,4 @@ if (Reanimated !== undefined && !Reanimated.setGestureState) {
132132
};
133133
}
134134

135-
// Reanimated resolves `getAnimatableRef()` once, when its ref to the wrapped
136-
// component attaches. Only versions that re-resolve it on update (their
137-
// `AnimatedComponent` has `_refreshComponentRef`) can be handed a child's host
138-
// instance that may change, as the legacy `Wrap` does. The class is not
139-
// exported, so probe it by subpath, as the v3 detector does for
140-
// `NativeEventsManager`.
141-
let refreshesAnimatableRef = false;
142-
try {
143-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
144-
const AnimatedComponent =
145-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
146-
require('react-native-reanimated/src/createAnimatedComponent/AnimatedComponent').default;
147-
refreshesAnimatableRef =
148-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
149-
typeof AnimatedComponent?.prototype?._refreshComponentRef === 'function';
150-
} catch {
151-
// Reanimated is missing or older; keep handing Reanimated the Wrap itself.
152-
}
153-
154-
export function reanimatedRefreshesAnimatableRef() {
155-
return refreshesAnimatableRef;
156-
}
157-
158135
export { Reanimated, Worklets };

0 commit comments

Comments
 (0)