Skip to content

compilePackages: class extending React.PureComponent doesn't inherit isReactComponent on its prototype (2-level chain) → react-reconciler treats error boundaries as function components → children dropped (blocks ink) #5024

Description

@proggeramlug

Summary

A class that extends React's PureComponent (i.e. two prototype levels up from React.Component) does not inherit the isReactComponent marker on its .prototype. react-reconciler's shouldConstruct(type) reads type.prototype.isReactComponent to decide ClassComponent (fiber tag 1) vs FunctionComponent (tag 0); when it reads undefined, the class is misclassified as a function component, its render() is never invoked as a class, and its children are dropped — the component renders nothing.

This is the wall after the Context.Provider classification fix (commit 81cfc6981, the labeled-break-from-nested-switch fix): ink's <ErrorBoundary> (class ErrorBoundary extends PureComponent) wraps the app's children, so the whole ink tree renders empty.

Minimal repro

import React from 'react';
const C: any = React.Component;
const PC: any = React.PureComponent;

console.log(typeof C.prototype.isReactComponent);          // Perry: "undefined"  (anomaly — see below)
console.log(typeof PC.prototype.isReactComponent);         // Perry: "undefined"

class X extends C  { render() { return null; } }
class Y extends PC { render() { return null; } }
console.log(typeof X.prototype.isReactComponent);          // Perry: "object"     ✓ (1 level: X → Component)
console.log(typeof Y.prototype.isReactComponent);          // Perry: "undefined"  ✗ (2 levels: Y → PureComponent → Component)

console.log(Object.getPrototypeOf(Y.prototype) === PC.prototype); // true
console.log(Object.getPrototypeOf(PC.prototype) === C.prototype); // true

Node prints object for all four isReactComponent reads.

The anomaly

React.Component.prototype.isReactComponent reads undefined directly, yet class X extends React.Component does observe it (object) via the chain. So the value is reachable one level up but:

  • a direct read on Component.prototype misses it, and
  • a two-level chain walk (Y → PureComponent → Component) misses it,

while a one-level walk (X → Component) finds it. The prototype links are all correct (getPrototypeOf checks pass). This points at a property-storage / prototype-chain-GET inconsistency specific to react's compiled component prototypes (PureComponent.prototype = Object.create(Component.prototype) shape), not generic inheritance — a plain user-land class Sub extends Base with Base.prototype.marker set does inherit correctly through two levels.

How it surfaced (ink, #348)

ink's App wraps children in <ErrorBoundary> (extends PureComponent). react-reconciler's shouldConstruct(ErrorBoundary)ErrorBoundary.prototype.isReactComponentundefinedshouldConstruct returns false → createFiberFromTypeAndProps leaves fiberTag = 0 (FunctionComponent) → ErrorBoundary is invoked as a function, its class render() never runs, this.props.children is never returned → the entire app subtree is dropped. Verified: return children from App renders; <ErrorBoundary>{children}</ErrorBoundary> renders nothing.

Impact

  • Every class component that extends PureComponent (and likely any ≥2-level React component subclass) — error boundaries, many older libraries. They silently render as no-op function components.
  • ink end-to-end (Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348): the wall after Context.Provider; ink lays out + renders natively otherwise (native taffy yoga + provider fix both working — text/boxes/colors render through bare/function/Fragment/Provider wrappers).

Related

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