Skip to content

extractInputs causes infinite recursion when worklet captures React Context in Jest #8913

@thomasttvo

Description

@thomasttvo

Description

extractInputs in mappers.ts causes infinite recursion when a worklet captures a React Context, because Context.Provider === Context (self-reference by design in React).

Steps to reproduce

git clone https://github.com/thomasttvo/reanimated-jest-extractinputs-repro
cd reanimated-jest-extractinputs-repro
yarn install
yarn test

Snack or a link to a repository

https://github.com/thomasttvo/reanimated-jest-extractinputs-repro

Reanimated version

3.19.4

React Native version

0.83.1

Platforms

iOS, Android (Jest environment)

JavaScript runtime

Hermes

Workflow

React Native CLI

Architecture

New Architecture

Build type

Debug

Device

iOS Simulator


Minimal Reproduction

import {createContext} from 'react';
import {useAnimatedStyle} from 'react-native-reanimated';

const MyContext = createContext(false);

function Component() {
  const animatedStyle = useAnimatedStyle(() => {
    const _ctx = MyContext; // Captured in worklet closure
    return { opacity: 1 };
  });
}

Root Cause

  1. React.createContext() returns { Provider: <self>, Consumer: {...} } where Context.Provider === Context
  2. Referencing Context inside a worklet → Babel plugin captures it in __closure
  3. extractInputs recursively iterates plain objects to find SharedValues
  4. Context is a plain object (prototype === Object.prototype)
  5. extractInputs iterates Context.Provider which equals Context → infinite recursion

Why This Doesn't Happen in Production

In production, makeShareableCloneRecursiveNative has cycle detection via caching.
In Jest, makeShareableCloneRecursiveWeb returns values unchanged (no cycle detection), so extractInputs receives raw objects with circular references.

Error

RangeError: Maximum call stack size exceeded
  at extractInputs (node_modules/react-native-reanimated/src/mappers.ts:140:25)
  at extractInputs (node_modules/react-native-reanimated/src/mappers.ts:155:20)
  at extractInputs ...

Related Issues

Suggested Fix

Add cycle detection (WeakSet) to extractInputs:

function extractInputs(
  inputs: unknown,
  resultArray: MapperExtractedInputs,
  visited = new WeakSet()
): MapperExtractedInputs {
  if (Array.isArray(inputs)) {
    for (const input of inputs) {
      if (input) {
        extractInputs(input, resultArray, visited);
      }
    }
  } else if (isSharedValue(inputs)) {
    resultArray.push(inputs);
  } else if (Object.getPrototypeOf(inputs) === Object.prototype) {
    if (visited.has(inputs as object)) {
      return resultArray; // Skip already-visited objects
    }
    visited.add(inputs as object);
    for (const element of Object.values(inputs as Record<string, unknown>)) {
      if (element) {
        extractInputs(element, resultArray, visited);
      }
    }
  }
  return resultArray;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Platform: AndroidThis issue is specific to AndroidPlatform: iOSThis issue is specific to iOSRepro providedA reproduction with a snippet of code, snack or repo is provided

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions