Description
@react-native/babel-preset pushes @babel/plugin-transform-destructuring unconditionally — including for Hermes targets (packages/react-native-babel-preset/src/configs/main.js, the always-on plugin list; verified in 0.81.4 at src/configs/main.js:122). Hermes supports array/object destructuring natively, so for Hermes builds the transform is (a) redundant bytecode and (b) a semantic hazard, because downleveled destructuring is not perfectly equivalent to native destructuring.
The concrete divergence: native array destructuring always goes through the iterator protocol, while Babel's _slicedToArray helper short-circuits via _arrayWithHoles — if (Array.isArray(r)) return r; — and then indexes positionally. For a Proxy over an array (Array.isArray pierces to the target) whose get trap serves a custom Symbol.iterator but does not answer positional reads, the two compile outputs produce different values: iterator yields the real elements, indexing yields undefined.
That exotic-sounding object is shipping in one of the most popular 3D libraries: three.js's TSL shader system passes Fn arguments through exactly such a Proxy, and three's own sources destructure it (src/nodes/accessors/Instance.js: Fn( ( [ instancedMesh ] ) => { const { instanceMatrix, instanceColor } = instancedMesh; ... } )). Metro rewrites three's code with this transform, so on device the body receives undefined and every React Native app using three.js WebGPU (react-native-webgpu, Dawn/Vulkan) with an InstancedMesh crashes on the first rendered frame:
TypeError: Cannot read property 'instanceMatrix' of undefined
at ... setupOutput / getOutputNode / build / flowNodeFromShaderStage / ... / render
The same pattern arms skinning(), batch() and morphReference() — i.e. skinned meshes, batched meshes and morph targets are equally affected. Nothing surfaces in Node/browser tests because those run untransformed code; the failure exists only in the Metro-bundled app.
Ask
Consider making transform-destructuring conditional on the engine target (skipped for Hermes, as several other transforms already are engine-aware), or documenting why it must stay unconditional. Cross-filed with the two neighboring projects, since each owns a slice of the collision:
Steps to reproduce
- Bare RN 0.81.4 app (Hermes, new arch) +
react-native-webgpu 0.8.2 + three 0.185.1
- Render any scene containing a
THREE.InstancedMesh with WebGPURenderer
- First frame throws
TypeError: Cannot read property 'instanceMatrix' of undefined
A toolchain-only demonstration (no device needed): run @react-native/babel-preset over const fn = ([a]) => a; and observe the _slicedToArray output; feed it an array-backed Proxy with a custom Symbol.iterator in its get trap and no index handling — native semantics return the element, transformed semantics return undefined.
React Native Version
0.81.4
Affected Platforms
Runtime: Android (verified on device: Android 15, Hermes release bundle, Dawn/Vulkan). iOS untested but the transform is platform-independent.
Output of npx @react-native-community/cli info
Not attached — the report is toolchain-level (@react-native/babel-preset 0.81.4, @babel/plugin-transform-destructuring 7.29.7, @babel/runtime 7.29.7); happy to add on request.
Description
@react-native/babel-presetpushes@babel/plugin-transform-destructuringunconditionally — including for Hermes targets (packages/react-native-babel-preset/src/configs/main.js, the always-on plugin list; verified in 0.81.4 atsrc/configs/main.js:122). Hermes supports array/object destructuring natively, so for Hermes builds the transform is (a) redundant bytecode and (b) a semantic hazard, because downleveled destructuring is not perfectly equivalent to native destructuring.The concrete divergence: native array destructuring always goes through the iterator protocol, while Babel's
_slicedToArrayhelper short-circuits via_arrayWithHoles—if (Array.isArray(r)) return r;— and then indexes positionally. For a Proxy over an array (Array.isArraypierces to the target) whosegettrap serves a customSymbol.iteratorbut does not answer positional reads, the two compile outputs produce different values: iterator yields the real elements, indexing yieldsundefined.That exotic-sounding object is shipping in one of the most popular 3D libraries: three.js's TSL shader system passes
Fnarguments through exactly such a Proxy, and three's own sources destructure it (src/nodes/accessors/Instance.js:Fn( ( [ instancedMesh ] ) => { const { instanceMatrix, instanceColor } = instancedMesh; ... } )). Metro rewrites three's code with this transform, so on device the body receivesundefinedand every React Native app using three.js WebGPU (react-native-webgpu, Dawn/Vulkan) with anInstancedMeshcrashes on the first rendered frame:The same pattern arms
skinning(),batch()andmorphReference()— i.e. skinned meshes, batched meshes and morph targets are equally affected. Nothing surfaces in Node/browser tests because those run untransformed code; the failure exists only in the Metro-bundled app.Ask
Consider making
transform-destructuringconditional on the engine target (skipped for Hermes, as several other transforms already are engine-aware), or documenting why it must stay unconditional. Cross-filed with the two neighboring projects, since each owns a slice of the collision:_arrayWithHolesfast path vs custom iterators on array-backed Proxies): link to follow in a commentSteps to reproduce
react-native-webgpu0.8.2 +three0.185.1THREE.InstancedMeshwithWebGPURendererTypeError: Cannot read property 'instanceMatrix' of undefinedA toolchain-only demonstration (no device needed): run
@react-native/babel-presetoverconst fn = ([a]) => a;and observe the_slicedToArrayoutput; feed it an array-backed Proxy with a customSymbol.iteratorin itsgettrap and no index handling — native semantics return the element, transformed semantics returnundefined.React Native Version
0.81.4
Affected Platforms
Runtime: Android (verified on device: Android 15, Hermes release bundle, Dawn/Vulkan). iOS untested but the transform is platform-independent.
Output of
npx @react-native-community/cli infoNot attached — the report is toolchain-level (
@react-native/babel-preset0.81.4,@babel/plugin-transform-destructuring7.29.7,@babel/runtime7.29.7); happy to add on request.