View Hierarchy Capture — runtime view-tree readback¶
Type: Reference (single contract document) Normative status: v1 contract surface — pattern contract; a conformant React-backed port MUST implement the walker against the read paths below. Non-React substrates (post-v1) MAY publish their own equivalent capture surface; this doc owns the React case. Locked: 2026-05-19 — Fiber-reading for hierarchy capture relaxed; per-component metadata reads remain rejected.
This doc pins the contract for runtime view-hierarchy capture — how a tool (today, Xray) reads the parent ⊃ children relationships of the views the host application has mounted. The contract is single-document because the React Fiber parent/child slots ARE the contract for every React-backed substrate the framework supports (Reagent / UIx / Helix all mount through React).
Per-adapter spec is intentionally absent — no adapter ships its own hierarchy-capture surface. The host's Fiber tree IS the source of truth; the walker reads it directly.
Scope¶
In scope:
- Reading the structural Fiber slots —
return,child,sibling,elementType— to reconstruct the parent ⊃ children view tree at any point in time. - Resolving each Fiber's
elementTypeto a re-frame2 view-id where the adapter has tagged the fn at registration time (the__rf2_view_id__property below); falling back todisplayName/name/ a"<host>"label otherwise. - Surfacing the captured tree to the host tool (Xray) for the Views panel's Group-by-tree toggle, parent-cascade attribution, and future Static Views surfaces.
Explicitly out of scope (REJECTED per Views Q1; STAYS REJECTED per Comment 6 lock-in):
- Reading per-component Fiber metadata — memo status, hook state (
memoizedState), lane priority, scheduler internals, work-in-progress slots. These are version-coupled to React internals that change every major release; the payoff is narrow (per-component drilldown only); the maintenance cost is high. Any tool that needs this data should ship its own React Profiler integration, NOT this contract.
The split mirrors the original Views Q1 decision: parent/child are the most stable Fiber slots (React DevTools relies on them; deliberately exposed via __REACT_DEVTOOLS_GLOBAL_HOOK__); everything else is volatile.
Read paths¶
Two read paths, in order of preference:
__REACT_DEVTOOLS_GLOBAL_HOOK__where available — React's deliberate devtools integration point. More stable than direct Fiber property reads because the hook's signature is published API to React DevTools and changes are coordinated with releases. If the hook is present, the walker uses it; otherwise it falls back to (2).- Direct
__reactFiber$*/__reactInternalInstance$*property reads on host DOM nodes. The walker discovers the property by scanning the DOM node's own keys for the documented prefix: - React 16:
__reactInternalInstance$<hash> - React 17+:
__reactFiber$<hash>
The two prefixes are React's documented Fiber-pointer scheme. The walker reads ONLY the four slots named under §Scope above; it does NOT inspect memoizedState, pendingProps, or any per-component metadata slot.
Production DCE — goog.DEBUG gate¶
Every Fiber-reading callsite MUST be wrapped in an (when interop/debug-enabled? …) form. The framework's goog.DEBUG-derived define collapses these branches under Closure :advanced so production bundles carry zero Fiber-reading code paths.
The walker is also dev-only by classpath: Xray's preload is :devtools/preloads-gated, so the walker namespace is not on the production classpath at all under the canonical install. The interop/debug-enabled? gate is the second line of defence against an accidental :require from a host's non-dev entry point.
Bundle-isolation contract — the walker must not leak to production bundles. The contract is enforced by implementation/scripts/check-bundle-isolation.cjs (the same sentinel-grep that pins per-feature artefact isolation). A non-zero hit on a walker sentinel in the examples/counter production bundle is a hard failure.
React-version regression check¶
Each React major bump (16 → 17 → 18 → 19 → …) MUST run a smoke test that confirms the walker still reads parent/child correctly. The smoke test lives in tools/xray/test/day8/re_frame2_xray/views/fiber_walker_cljs_test.cljs and stubs a minimal Fiber-shaped object graph that mirrors the React version's published structure. If the smoke breaks, the choice is binary:
- Ship a fix — update the walker to the new Fiber slot names / shape. This is the expected path when React renames a slot but keeps the structural model.
- Fall back to data-attribute tagging — each
reg-viewmutates its first element's attribute map to includedata-rf-view="<name>"; the walker queriesdocument.querySelectorAll('[data-rf-view]')and infers parent ⊃ children by DOM containment. This is the React-version-independent escape hatch.
The fallback is shipped and dormant — the tagging is wired into every adapter's render-time wrapper (Reagent via the inline hiccup walk, UIx + Helix via the spine's React.cloneElement wrapper); the contract pins the attribute format and the documented edge cases at Spec 006 §View tagging contract. It is the second-best option (fragments invisible, portals teleport-broken, requires per-adapter wrap-view cost in dev builds) and consumers should default to the Fiber walker.
View-id tagging convention¶
A re-frame2 view registered via rf/reg-view SHOULD carry the view-id keyword on its compiled function under the property __rf2_view_id__. The walker reads this property without dragging an adapter :require into the walker namespace (the property is set at registration time by core's reg-view machinery).
When the property is absent (anonymous fn, plain host element, fragment, third-party component), the walker falls back through:
:displayName(React-conventional):name(function-name)"<host>"(literal label for host elements like:div)
The fallback keeps the tree rendering meaningful even for un-tagged elements — the row appears but does not resolve to a re-frame2 view-id, so the Views panel's per-row drilldown is not available for that row.
Capture algorithm¶
The walk is a strict depth-first traversal over the React Fiber tree, rooted at the host application's mount node. The walker:
- Resolves the root Fiber. From the configured mount node (a DOM element handed in by the host tool), the walker reads the documented Fiber-pointer property (
__reactFiber$<hash>on React 17+,__reactInternalInstance$<hash>on React 16). The first Fiber discovered through__REACT_DEVTOOLS_GLOBAL_HOOK__(when present) takes precedence over the direct property read. - Walks
child/siblingrecursively. At each Fiber the walker visitschildfirst (descending), thensibling(advancing to the next peer once a subtree is exhausted).returnis read only when needed to compute parent-relative depth; the walker MUST NOT followreturnupward as part of normal traversal — the recursion's own stack carries depth. - Resolves each Fiber's
elementTypeto a view-id per §View-id tagging convention — fallback chain__rf2_view_id__→:displayName→:name→"<host>". - Emits one record per Fiber in document order (the order the recursion visits them) into the output vector documented at §Output shape below.
- Halts on any unreadable slot. A null
childends descent; a nullsiblingends the peer walk; a Fiber whoseelementTypecannot be resolved still emits a record with:view-id nil(the fallback"<host>"covers the common case). The walker MUST NOT throw on a malformed Fiber — it elides the offending subtree and continues.
The walker reads only the four slots listed under §Scope (return, child, sibling, elementType). Per-component metadata slots (memoizedState, pendingProps, lane priority) are out of scope per the same section and the walker MUST NOT read them — a port that does is non-conformant.
Re-walks are stateless: the walker holds no inter-call state. A repeated capture against the same Fiber tree produces an identical output (modulo :fiber-keys where React replaced a Fiber as part of a remount). Tools that need stable identity across cascades use :fiber-key as the React-side row key per §Output shape.
Output shape¶
The walker produces a depth-first vector of:
{:view-id <keyword | string | nil> ;; resolved per §View-id tagging
:depth <non-negative integer> ;; 0 = root Fiber
:fiber-key <integer>} ;; stable hash of the Fiber pointer
In document order. :fiber-key is used as the React key for tree-row rendering so re-walks across cascades preserve row identity (toggle state, expansion state) where the Fiber pointer is stable.
What this unlocks¶
Per the findings §12 lock-in:
- Group-by-tree toggle in the Xray Views panel — third toggle alongside Group-by-component and Group-by-sub. Parent ⊃ children indentation; collapsible "X (47 descendants re-rendered)" rollup rows.
- Mount/unmount cascade attribution — single row per cascade instead of N rows.
- Future Static Views surface — a Static sub-tab that browses registered views and shows their typical render hierarchy, viable now that runtime hierarchy is captureable.
- Per-tree-node click-to-source — combined with the existing source-coord lift, tree nodes carry their own jump-to-source affordance.
Ownership + cross-references¶
| Surface | Owner |
|---|---|
| Fiber-walker implementation (CLJS) | tools/xray/src/day8/re_frame2_xray/views/fiber_walker.cljs |
| Group-by-tree renderer | tools/xray/src/day8/re_frame2_xray/views/group_by_tree.cljs |
| Views panel toggle wiring | tools/xray/src/day8/re_frame2_xray/panels/views_view.cljs |
| React-version regression smoke | tools/xray/test/day8/re_frame2_xray/views/fiber_walker_cljs_test.cljs |
| Production DCE contract | implementation/scripts/check-bundle-isolation.cjs |
| Fallback (data-attribute tagging) | Bead ``; documented in findings §12.1 |
| Reactive-substrate adapter API | 006-ReactiveSubstrate.md (Fiber is the contract, not an adapter-side surface) |
| Xray Views panel | tools/xray/spec/012-Views.md |
Decisions log¶
- 2026-05-19 ~14:55 AUSEST — Mike LOCKS Fiber-reading for parent/child hierarchy capture. Per-component metadata reads STAY REJECTED. Comments 4–5 (data-attribute tagging as primary) deprecated to fallback status. (Findings doc §11 Comment 6, §12; bead ``.)
- 2026-05-19 — Walker implementation lands behind
interop/debug-enabled?gate; React-version smoke test seeded for React 16 + 17+. Production DCE verified vianpm run test:bundle-isolation. (Bead ``.)