Skip to content

compilePackages: cross-module mutable object loses identity — react-reconciler's ReactSharedInternals.H dispatcher invisible to react → 'Invalid hook call' null dispatcher (blocks all React renderers) #4950

@proggeramlug

Description

@proggeramlug

Summary

React's hook dispatcher is null at render time under Perry, because the mutable ReactSharedInternals object that react exports and react-reconciler mutates is not the same object instance in the two modules. Rendering any component that uses a hook throws React's "Invalid hook call" path:

Invalid hook call. Hooks can only be called inside of the body of a function component...
3. You might have more than one copy of React in the same app
TypeError: Cannot read properties of null (reading 'useContext')

This is the first render-time ink wall — a big milestone: with the prior fixes (#4902 box-pointer, #4947 class-export, the regex/Segmenter/MessageChannel fixes) the entire ink + react@19 + react-reconciler@0.33 dependency graph now initializes, render() is reached and invoked, and the failure is now inside React's reconciliation rather than at module init.

Mechanism (verified against the compiled prod builds)

  • react/cjs/react.production.js:353 exports the shared-internals singleton:
    exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals
  • react's hooks read the current dispatcher from it: ReactSharedInternals.H (react.production.js:358, 477-502, …). When ReactSharedInternals.H is null, useContext does null.useContext → exactly the observed Cannot read properties of null (reading 'useContext').
  • react-reconciler/cjs/react-reconciler.production.js:10347 captures it:
    var ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
    and installs the dispatcher by mutating it during render: ReactSharedInternals.H = <HooksDispatcher> (:11287, 11291, 11295, 11299, 11304, 11309).

For this to work, react-reconciler's ReactSharedInternals and react's internal ReactSharedInternals must be the same object — react-reconciler sets .H, react reads .H. The null-dispatcher error means Perry hands react-reconciler a different object than react uses internally: i.e. reading the exported member React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE across the module boundary yields a copy, not the same instance, so the .H mutation is invisible to react.

This is the real form of React's "more than one copy of React" warning here: not two react copies, but a cross-module mutable-singleton identity break — Perry isn't preserving object identity for a value exported by one compiled module and read by another.

Repro (ink — authoritative)

import React from 'react';
import { render, Text } from 'ink';
render(React.createElement(() => React.createElement(Text, null, 'hi')));
// → Invalid hook call / Cannot read properties of null (reading 'useContext')

(ink's internal components — <App>, the context providers — call useContext/hooks during the first commit.)

Scope / suggested triage

  • The invariant to restore: a mutable object exported from compiled module A (react) and read via a namespace/member access in compiled module B (react-reconciler) must be the same instance, so writes through B's reference are visible to A. Check whether compilePackages cross-module value reads deep-copy or re-box objects (this would also affect any library relying on a shared mutable singleton, e.g. module-level registries/caches).
  • Confirming probe: have B write a sentinel to React.__CLIENT_INTERNALS_….H and have A read it back; if A still sees the old value, identity is not preserved.

Impact

  • Every custom React renderer (ink, react-three-fiber, react-pdf, react-nil, react-test-renderer) — they all install the dispatcher via ReactSharedInternals.H and rely on the shared-singleton identity. This is the gate for all of them.
  • ink end-to-end (Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348): first render-time wall; after it, the next gate is yoga-layout's WASM runtime (out-of-scope).

Secondary note (not this issue)

A synthetic minimal react-reconciler render harness (no ink) instead tripped TypeError: AbortController is not a function at init — a separate gap that only appeared in that reduced graph; mentioning for awareness, not filing yet.

Related

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