Skip to content

animated-value-initialization corrupts the 2nd+ occurrence of the same pattern in a file (crashes react-native-screens) #115

Description

@dzengi

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions