Description
animated-value-initialization (the optimizer that rewrites useRef(new Animated.Value(0)).current into the built-in useAnimatedValue(0) hook) corrupts every occurrence after the first one when the same pattern appears more than once in a single function.
Reproduction
react-native-screens' InnerScreen (src/components/Screen.tsx) has three back-to-back instances of the pattern:
const closing = React.useRef(new Animated.Value(0)).current;
const progress = React.useRef(new Animated.Value(0)).current;
const goingForward = React.useRef(new Animated.Value(0)).current;
With animated-value-initialization enabled (the default for React Native 0.83+), Metro compiles this to:
var closing = (0, _reactNative.useAnimatedValue)(0);
var progress = _reactNative(0); // <- calls the namespace import as a function
var goingForward = _reactNative(0); // <- same here
Only the first occurrence gets the correct (0, _reactNative.useAnimatedValue)(0) call. The second and third become a bare call on the _reactNative namespace object itself, which throws at runtime:
Render Error
Object is not a function
This crashes on every screen mount, since InnerScreen is used by every react-navigation/react-native-screens screen.
Root cause
In animated-value-initialization's MemberExpression visitor (src/plugin → compiled to dist/plugin/index.js), the hook identifier returned by addFileImportHint is cached per nameHint on the file (to avoid inserting duplicate imports) and then reused by reference as the callee for every matching call site:
const hook = addFileImportHint({ file, nameHint: hookName, path, importName: hookName, moduleName });
const replacement = core.types.callExpression(
hook, // <- same AST node object reused as callee at every call site
constructor.expression.arguments.map((argument) => core.types.cloneNode(argument, true))
);
Because the exact same node object ends up as the callee of multiple distinct CallExpressions in the tree, a later Babel pass (the commonjs/interop transform that wraps a namespace-member call in (0, ns.method)(...)) only transforms it correctly the first time it visits that node instance and leaves the other call sites referencing the untransformed namespace object.
Fix
Cloning the import reference at each use site fixes it:
const replacement = core.types.callExpression(
- hook,
+ core.types.cloneNode(hook, true),
constructor.expression.arguments.map((argument) => core.types.cloneNode(argument, true))
);
We're carrying this as a local patch-package patch for now. Happy to open a PR with this one-line fix if useful.
Environment
react-native-boost: 2.0.2
react-native: 0.86.3
react-native-screens: 4.28.0
- Config:
withBoostConfig via Metro (react-native-boost/metro), default optimizations, Expo SDK 57
Description
animated-value-initialization(the optimizer that rewritesuseRef(new Animated.Value(0)).currentinto the built-inuseAnimatedValue(0)hook) corrupts every occurrence after the first one when the same pattern appears more than once in a single function.Reproduction
react-native-screens'InnerScreen(src/components/Screen.tsx) has three back-to-back instances of the pattern:With
animated-value-initializationenabled (the default for React Native 0.83+), Metro compiles this to:Only the first occurrence gets the correct
(0, _reactNative.useAnimatedValue)(0)call. The second and third become a bare call on the_reactNativenamespace object itself, which throws at runtime:This crashes on every screen mount, since
InnerScreenis used by everyreact-navigation/react-native-screensscreen.Root cause
In
animated-value-initialization'sMemberExpressionvisitor (src/plugin→ compiled todist/plugin/index.js), thehookidentifier returned byaddFileImportHintis cached pernameHinton the file (to avoid inserting duplicate imports) and then reused by reference as the callee for every matching call site:Because the exact same node object ends up as the
calleeof multiple distinctCallExpressions in the tree, a later Babel pass (the commonjs/interop transform that wraps a namespace-member call in(0, ns.method)(...)) only transforms it correctly the first time it visits that node instance and leaves the other call sites referencing the untransformed namespace object.Fix
Cloning the import reference at each use site fixes it:
We're carrying this as a local
patch-packagepatch for now. Happy to open a PR with this one-line fix if useful.Environment
react-native-boost: 2.0.2react-native: 0.86.3react-native-screens: 4.28.0withBoostConfigvia Metro (react-native-boost/metro), default optimizations, Expo SDK 57