Reconcile the Two.js scene graph with React updates - #36
Conversation
…ode lifecycle (fix #29)
There was a problem hiding this comment.
🟡 Changes recommended
useTwoObject performs parent/registry mutations during render for recreation, which can violate React render guarantees and risk corrupting scene graph state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a centralized reconciliation layer so React updates (reorder, reparent, prop diff/removal, construction-prop replacement, and Strict Mode behavior) keep the underlying Two.js scene graph consistent after the initial mount.
Changes:
- Added a shared reconciliation core (
diffProps, default capture/reset, and in-place scene order reconciliation). - Introduced
useTwoObject/useTwoGrouphooks and refactored wrappers to use them. - Added a dedicated test suite validating Issue #29’s reconciliation semantics.
File summaries
| File | Description |
|---|---|
| tests/reconciliation.test.tsx | Adds coverage for ordering, reparenting, discrete prop diffing/resets, construction-prop replacement, and Strict Mode lifecycle behavior. |
| lib/useTwoObject.ts | Implements shared object lifecycle (create/recreate, prop diff/apply/reset, attachment/reparenting, child order registration, event registration). |
| lib/reconciliation.ts | Defines default prop semantics, captures per-instance defaults, computes discrete diffs, and reconciles children order in-place. |
| lib/Context.ts | Extends parent context with child attach/detach and sibling order registration; adds ChildSlotContext for memo-safe reorders. |
| lib/Provider.tsx | Implements root-level attach/detach/order registration and reconciles root scene order; wires ChildSlotContext providers. |
| lib/main.ts | Exports reconciliation internals (useTwoObject, useTwoGroup, TWO_DEFAULT_PROPS). |
| lib/Group.tsx | Refactors Group wrapper to use useTwoGroup and slot-based child rendering. |
| lib/SVG.tsx | Refactors SVG wrapper to use useTwoGroup; updates async load flow and child rendering. |
| lib/Circle.tsx | Refactors Circle wrapper to useTwoObject (construction prop: resolution). |
| lib/Rectangle.tsx | Refactors Rectangle wrapper to useTwoObject. |
| lib/RoundedRectangle.tsx | Refactors RoundedRectangle wrapper to useTwoObject. |
| lib/Ellipse.tsx | Refactors Ellipse wrapper to useTwoObject (construction prop: resolution). |
| lib/Line.tsx | Refactors Line wrapper to useTwoObject with special endpoint prop handling. |
| lib/Path.tsx | Refactors Path wrapper to useTwoObject with special manual handling. |
| lib/Points.tsx | Refactors Points wrapper to useTwoObject. |
| lib/Polygon.tsx | Refactors Polygon wrapper to useTwoObject (construction prop: sides). |
| lib/Star.tsx | Refactors Star wrapper to useTwoObject. |
| lib/ArcSegment.tsx | Refactors ArcSegment wrapper to useTwoObject (construction prop: resolution). |
| lib/Text.tsx | Refactors Text wrapper to useTwoObject. |
| lib/Image.tsx | Refactors Image wrapper to useTwoObject (construction prop: src; special mode/texture). |
| lib/Sprite.tsx | Refactors Sprite wrapper to useTwoObject (construction prop: src; special autoPlay; pause on dispose). |
| lib/ImageSequence.tsx | Refactors ImageSequence wrapper to useTwoObject (construction prop: src; special autoPlay; pause on dispose). |
| lib/LinearGradient.tsx | Refactors LinearGradient wrapper to useTwoObject (non-scene object; special endpoint props). |
| lib/RadialGradient.tsx | Refactors RadialGradient wrapper to useTwoObject (non-scene object; special center/focal props). |
| lib/Texture.tsx | Refactors Texture wrapper to useTwoObject (non-scene object; construction prop: src). |
Review details
- Files reviewed: 25/25 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…llow SVG branch, and broaden Texture src type
There was a problem hiding this comment.
🟡 Changes recommended
A confirmed edge case in useTwoObject can leak/duplicate children when construction-prop recreation coincides with a parent change.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 25/25 changed files
- Comments generated: 2
- Review effort level: Lite
… remove shallow from SVG deps
There was a problem hiding this comment.
🔵 Needs a closer look
Prop-removal reset semantics are incomplete for some publicly supported props (e.g. position/skewX/skewY/matrix/worldMatrix), which can leave stale values on instances after props are removed.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
lib/reconciliation.ts:29
useTwoObjectresets removed props usingdefaultPropsRef/TWO_DEFAULT_PROPS, butcaptureDefaultProps()only captures keys listed inTWO_DEFAULT_PROPS. Several props that are publicly exposed viaShapeProps(e.g.position,skewX,skewY,matrix,worldMatrixinlib/Properties.ts) are missing here, so removing those props will leave stale values on the Two.js instance (no reset occurs becausedefaultValstaysundefined). Adding these keys toTWO_DEFAULT_PROPS(even withundefinedplaceholders for non-primitives) allowscaptureDefaultProps()to snapshot them and makes prop-removal resets work consistently.
- Files reviewed: 25/25 changed files
- Comments generated: 0 new
- Review effort level: Lite
# Conflicts: # lib/Group.tsx # lib/Image.tsx # lib/ImageSequence.tsx # lib/Path.tsx # lib/Points.tsx # lib/Rectangle.tsx # lib/SVG.tsx # lib/Sprite.tsx # lib/Text.tsx
Walkthrough: React Two.js Scene Graph Reconciliation (Issue #29)
We resolved GitHub Issue #29 on branch
fix_issue_29_react, addressing all 9 acceptance criteria for React-controlled Two.js scene-graph reconciliation, prop resets, discrete prop diffing, in-place construction prop updates, and Strict Mode lifecycle safety.1. Summary of Changes
A. Centralized Reconciliation Core
TWO_DEFAULT_PROPScapturing standard Two.js defaults (fill: '#fff',stroke: '#000',linewidth: 1,opacity: 1,visible: true,x: 0,y: 0, etc.).captureDefaultProps(instance)to snapshot initial properties of any instantiated Two.js shape.diffProps(prev, next, ignored)for discrete prop diffing, partitioning properties intochangedandremoved.reconcileSceneOrder(parent, targetOrder)to sortparent.childrenin-place and setparent._flagOrder = true.B. Shared Lifecycle Hooks
useTwoObjecthook:resolution,sides,src) change, performing in-place replacement at the identical index inparent.children.undefined.parent.childrenattachment and detachment when parent changes.registerChildOrderduring commit layout effect.useTwoGrouphook for container components (Group,SVG,Provider):reconcileSceneOrder(instance, childrenOrderRef.current).ChildSlotContext.Providerso memoized children (React.memo) detect sibling reorders.C. Context & Provider Architecture
TwoParentContextValuewithattachChild,detachChild,registerChildOrder.ChildSlotContextfor sibling position notification across memo boundaries.attachChild,detachChild,registerChildOrderfor the roottwo.scene.reconcileSceneOrderduring root layout effect.ChildSlotContext.Provider.D. Wrapper Modernization (Deleted >1,400 lines of duplication)
Refactored all wrappers to use
useTwoObjectanduseTwoGroup:constructionProps: ['resolution'])constructionProps: ['resolution'])specialProps: ['x1', 'y1', 'x2', 'y2'])specialProps: ['manual'])constructionProps: ['sides'])constructionProps: ['resolution'])constructionProps: ['src'])constructionProps: ['src'])constructionProps: ['src'])isSceneObject: false)isSceneObject: false)isSceneObject: false)2. Verification & Acceptance Criteria
We created tests/reconciliation.test.tsx to explicitly verify all criteria:
Group.childrenorderreorders parent.children when keyed JSX siblings reorderReact.memocomponentsreorders parent.children even when sibling components are wrapped in React.memomoves a keyed object between groups, removing from old parent and inserting into new parent&reparents the same component instance when parent context changes dynamicallyupdates only changed properties without reassigning unchanged properties(verified_flagStrokeand_flagFillremain untriggered)restores Two.js default values when props are removed or set to undefined&restores default values on Text component when props are removedrecreates instance safely in-place at the exact child index when construction props changehandles StrictMode double-mount without leaking duplicate children or event handlersdistinguishes owned resources from shared resources on unmountTest Suite Execution
Build & Lint
eslint .: 0 errors, 0 warnings.npm run build: built production bundles and type declarations cleanly.