Summary
Rendering anything through react-reconciler (0.33.0) + react (19.x) throws TypeError: value is not a function inside updateContainerSync. The reconciler builds and a container is created fine; the failure is in the synchronous render/commit path. Confirmed (via native backtrace) to be react-reconciler's production build — so it's a genuine codegen gap, not a dev-only path.
This is the wall immediately after #5009/#5014 (which correctly switched react-reconciler to its production build). It's the last thing between ink (#348) and yoga layout, and it blocks every custom React renderer.
Minimal repro (no ink, no yoga — ~30 lines)
import React from 'react';
import createReconciler from 'react-reconciler';
const noop = () => {};
const rec = createReconciler({
supportsMutation: true, isPrimaryRenderer: true, supportsPersistence: false, supportsHydration: false, supportsMicrotasks: true,
getRootHostContext: () => ({}), getChildHostContext: () => ({}), getPublicInstance: (i)=>i,
prepareForCommit: () => null, resetAfterCommit: noop, clearContainer: noop,
createInstance: () => ({ children: [] }), createTextInstance: () => ({ text: '' }),
appendInitialChild: noop, finalizeInitialChildren: () => false, shouldSetTextContent: () => false,
appendChild: noop, appendChildToContainer: noop, insertBefore: noop, removeChild: noop, removeChildFromContainer: noop,
commitUpdate: noop, commitTextUpdate: noop, detachDeletedInstance: noop, getCurrentEventPriority: () => 0,
maySuspendCommit: () => false, scheduleMicrotask: queueMicrotask, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1,
preparePortalMount: noop, prepareScopeUpdate: noop, getInstanceFromScope: () => null, getInstanceFromNode: () => null,
beforeActiveInstanceBlur: noop, afterActiveInstanceBlur: noop,
});
const container = rec.createContainer({ children: [] }, 0, null, false, null, 'id', ()=>{}, ()=>{}, ()=>{}, null);
function App() { return React.createElement('host', null, 'hi'); }
rec.updateContainerSync(React.createElement(App), container, null, noop); // → TypeError: value is not a function
rec.flushSyncWork();
(compilePackages: ["react","react-reconciler","scheduler"] + allow.compilePackages, PERRY_TREE_SHAKE=1, perry.define NODE_ENV=production.)
Output:
[A] reconciler built
[B] container created
[Cx] render threw: value is not a function
Native backtrace (--debug-symbols, breakpoint on throw_not_callable)
#0 perry_runtime::closure::dispatch::throw_not_callable
#1 js_closure_call1
#2 perry_closure_node_modules_react_reconciler_cjs_react_reconciler_PRODUCTION_js__96
#3 …react_reconciler_PRODUCTION_js__89
#4 …react_reconciler_PRODUCTION_js__403
#5 …react_reconciler_PRODUCTION_js__458
#6 …react_reconciler_PRODUCTION_js__548
#7 js_native_call_value
#8 js_native_call_method
#9 Ink.render / (in the minimal repro: updateContainerSync)
So react-reconciler.production.js closure __96, reached via __548 → __458 → __403 → __89, calls a value through js_closure_call1 that Perry finds non-callable. (Registers are unavailable in the optimized frames, so the exact callee value couldn't be read from lldb — the closure indices __96/__89/__403/__458/__548 should map to source via --trace llvm on react_reconciler_cjs_react_reconciler_production_js.ll.)
Notes / triage
Impact
Related
Summary
Rendering anything through
react-reconciler(0.33.0) +react(19.x) throwsTypeError: value is not a functioninsideupdateContainerSync. The reconciler builds and a container is created fine; the failure is in the synchronous render/commit path. Confirmed (via native backtrace) to be react-reconciler's production build — so it's a genuine codegen gap, not a dev-only path.This is the wall immediately after #5009/#5014 (which correctly switched react-reconciler to its production build). It's the last thing between ink (#348) and yoga layout, and it blocks every custom React renderer.
Minimal repro (no ink, no yoga — ~30 lines)
(
compilePackages: ["react","react-reconciler","scheduler"]+allow.compilePackages,PERRY_TREE_SHAKE=1,perry.defineNODE_ENV=production.)Output:
Native backtrace (
--debug-symbols, breakpoint onthrow_not_callable)So
react-reconciler.production.jsclosure__96, reached via__548 → __458 → __403 → __89, calls a value throughjs_closure_call1that Perry finds non-callable. (Registers are unavailable in the optimized frames, so the exact callee value couldn't be read from lldb — the closure indices__96/__89/__403/__458/__548should map to source via--trace llvmonreact_reconciler_cjs_react_reconciler_production_js.ll.)Notes / triage
updateContainerSyncrender+commit, not during setup.js_closure_call1is being handed a value that should be a bound method / closure but arrives as a non-callable (e.g. a captured/aliased function that lost its callable identity) — same family as the cross-module identity issues seen earlier in this chain (compilePackages: cross-module mutable object loses identity — react-reconciler'sReactSharedInternals.Hdispatcher invisible to react → 'Invalid hook call' null dispatcher (blocks all React renderers) #4950, Captured Object.assign alias causes react-reconciler HostRoot render hang #2564).Impact
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348): the final wall before yoga layout (the out-of-scope WASM terminal).Related
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348 (ink end-to-end), regression: perry.define ofprocess.env.NODE_ENVno longer applied (ignored since process became a real object) → React/react-reconciler load dev builds → 'value is not a function' crash (blocks ink) #5009/fix(hir): perry.define of process.env.* folds at lowering, not just tree-shake (#5009) #5014 (define fix that routed to the production build, exposing this)ReactSharedInternals.Hdispatcher invisible to react → 'Invalid hook call' null dispatcher (blocks all React renderers) #4950 / Captured Object.assign alias causes react-reconciler HostRoot render hang #2564 (react-reconciler / cross-module callable-identity family)