Skip to content

Captured Object.assign alias causes react-reconciler HostRoot render hang #2564

@andrewtdiz

Description

@andrewtdiz

Summary

Using latest npm Perry (@perryts/perry@0.5.1025) with React 19's reconciler, basic package import/root creation works, but the first real Fiber render hangs.

The smallest failing surface I found is react-reconciler HostRoot update processing:

const root = Renderer.createContainer(container, LegacyRoot, null, false, null, "id", onError, onError, onError, null);

Renderer.updateContainerSync(null, root, null, () => {});
Renderer.flushSyncWork(); // hangs under Perry

This reproduces even with null, so the failure is before host component creation, text nodes, function components, or mutation methods.

Versions

@perryts/perry@0.5.1025
react@19.2.6
react-reconciler@0.33.0
scheduler@0.27.0

The repro was bundled with esbuild to avoid CJS package loading noise.

Evidence

Instrumenting the bundled react-reconciler.production.js shows Perry reaches HostRoot and then throws inside processUpdateQueue:

trace: updateContainerSync.before
trace: updateContainerSync.after
trace: flushSyncWork.before
internal.performUnitOfWork.enter tag=3
internal.pushHostContainer.afterContextPush
internal.hostRoot.beforeCloneUpdateQueue
internal.hostRoot.afterCloneUpdateQueue
internal.processUpdateQueue.enter
internal.processUpdateQueue.loop tag=0 lane=2
internal.processUpdateQueue.case0 beforePayload payloadType=object
internal.processUpdateQueue.case0 beforeAssign assignType=number newStateType=object updateLaneType=object
internal.renderRootSync.catch value=[object Object] type=object then=undefined
internal.renderRootSync.catch value=[object Object] type=object then=undefined
...

The React reconciler code at that point is:

var assign = Object.assign;

// inside processUpdateQueue(...)
newState = assign({}, newState, update.payload);

Under Perry, that call throws, React catches the thrown object in renderRootSync, retries, and loops forever. If I patch just that call to direct Object.assign({}, newState, update.payload), Perry advances past HostRoot update processing and reaches renderer host calls like getChildHostContext, shouldSetTextContent, and createInstance.

Expected

Captured stdlib function aliases like this should remain callable and equivalent to the member call:

var assign = Object.assign;
assign({}, a, b);

React packages commonly capture stdlib members this way.

Guidance

Please investigate Perry's lowering/runtime representation for function-valued stdlib member references captured into locals, especially inside bundled CommonJS wrapper scopes. The failing bundle has multiple local assign = Object.assign bindings from React and react-reconciler; at the failing call site Perry reports the local alias as number and the call throws.

It would also help if thrown non-Error objects preserved enough diagnostic information to avoid this kind of silent retry loop. React's sync work loop keeps retrying because the thrown value is opaque ([object Object]) and not a thenable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions