Files
lemonspace_app/docs/plans/2026-04-03-canvas-modularization-design.md

8.2 KiB

Canvas Modularization Design

Goal

Modularize components/canvas/canvas.tsx so the file becomes easier to read, easier to test, and easier for a second engineer to navigate without changing user-visible canvas behavior or existing public component contracts.

Why Now

  • components/canvas/canvas.tsx currently combines at least four different responsibilities: sync orchestration, Convex-to-React-Flow reconciliation, interaction handlers, and render composition.
  • The file is still the main knowledge bottleneck in the canvas engine even after earlier helper extractions.
  • A second developer joining during the seed phase will need clearer ownership boundaries and faster onboarding into the canvas hot path.

Current Findings

  • Sync and optimistic update orchestration are concentrated around the mutation and queue layer in components/canvas/canvas.tsx:258 and components/canvas/canvas.tsx:984.
  • Convex-to-local reconciliation for nodes and edges lives in large useLayoutEffect blocks in components/canvas/canvas.tsx:1922.
  • Interaction logic for drag, connect, reconnect, drop, scissor mode, and highlighting is spread across the lower half of the file in components/canvas/canvas.tsx:1798, components/canvas/canvas.tsx:2182, components/canvas/canvas.tsx:2555, and components/canvas/canvas.tsx:2730.
  • Render composition is comparatively simple, but it is buried under orchestration code in components/canvas/canvas.tsx:2901.

Context7 Guidance Applied

  • React guidance: extract non-visual logic into custom hooks so components focus on rendering and explicit data flow.
  • React guidance: keep hook composition clear and stable rather than hiding too much behavior inside a generic controller.
  • React Flow guidance: keep nodeTypes stable outside render, preserve ReactFlowProvider boundaries, and keep controlled flow state explicit when splitting handlers.

Chosen Approach

Use a thin-orchestrator refactor:

  1. Keep components/canvas/canvas.tsx as the top-level canvas entry and wiring layer.
  2. Extract the largest logic clusters into domain-specific hooks with explicit inputs and outputs.
  3. Leave existing external APIs unchanged: Canvas, CanvasInner, provider usage, mutation contracts, and React Flow integration stay behaviorally identical.
  4. Prefer hook boundaries over a single giant controller or reducer in phase 1.

This approach gives the best trade-off between readability, testability, and migration risk.

Rejected Alternatives

1. Single useCanvasController hook with reducer

  • Pros: very strong test surface and centralized state transitions.
  • Cons: too risky for the current canvas because drag locks, optimistic ID handoff, offline replay, and Convex reconciliation are behavior-sensitive and already intertwined with refs and effects.

2. Minimal handler extraction only

  • Pros: lowest refactor risk.
  • Cons: improves file length but not ownership or conceptual clarity; canvas.tsx would still remain the main cognitive bottleneck.

Target Architecture

1. canvas.tsx becomes the orchestrator

components/canvas/canvas.tsx should keep only:

  • top-level provider wiring
  • hook composition
  • final prop assembly for the rendered canvas view
  • small glue callbacks when multiple hooks need to meet

The file should stop owning detailed effect bodies and long imperative flows.

2. Extract domain hooks by responsibility

components/canvas/use-canvas-sync-engine.ts

Owns:

  • online/offline state
  • queue flushing and retry scheduling
  • deferred mutation handling for optimistic nodes
  • optimistic-to-real ID remapping support
  • wrappers like runMoveNodeMutation, runResizeNodeMutation, runUpdateNodeDataMutation, runCreateEdgeMutation, and node creation variants

Why:

  • This is the most operationally complex part of the file.
  • It has clear inputs and outputs and can be tested with mocked mutations and queue helpers.

components/canvas/use-canvas-flow-reconciliation.ts

Owns:

  • Convex edges -> RF edges reconciliation
  • Convex nodes -> RF nodes reconciliation
  • carry-forward logic for optimistic edges during handoff gaps
  • local pinning and merge behavior
  • compare-node data resolution and glow-aware edge mapping

Why:

  • This logic is mostly deterministic and should become easier to test if helper functions are extracted out of effect bodies.

components/canvas/use-canvas-node-interactions.ts

Owns:

  • onNodesChange
  • node drag start / drag / drag stop
  • resize lock transitions
  • intersected-edge highlighting state

Why:

  • Today the drag lifecycle is hard to reason about because local visual behavior and persistence behavior are mixed together.

components/canvas/use-canvas-connections.ts

Owns:

  • onConnect
  • onConnectEnd
  • connection-drop-menu state
  • create-node-from-connection flows
  • adapter glue for reconnect handling

Why:

  • New connection creation is a separate mental model from drag/drop creation and should be independently understandable.

components/canvas/use-canvas-drop.ts

Owns:

  • onDragOver
  • onDrop
  • DnD payload parsing
  • file upload drop flow
  • create-node-on-drop orchestration

Why:

  • Drop behavior has its own error handling, upload behavior, and defaults. It is a natural isolation boundary.

3. Optional view extraction in the final phase

Add components/canvas/canvas-view.tsx only after the main hook boundaries are stable.

It should stay presentational and receive:

  • nodes, edges
  • overlay state
  • ReactFlow handlers
  • toolbar state and callbacks

Why not first:

  • Extracting view first reduces file size but leaves the hard behavioral logic untouched.

Ownership Model For A Two-Engineer Team

The proposed split creates clean ownership zones:

  • Engineer A: sync, optimistic state, offline behavior
  • Engineer B: interactions, connection creation, drop flows
  • Shared: top-level composition in canvas.tsx and pure helpers in canvas-helpers.ts

This is useful even if the second engineer joins later because it documents the system in code boundaries, not only in docs.

Testing Strategy

The refactor should raise confidence by moving logic into units that are easier to test.

Priority test targets

  • optimistic node create -> real ID remap
  • deferred resizeNode and updateData for optimistic nodes
  • edge carry during the create/handoff gap
  • drag lock prevents stale Convex snapshots from overriding local drag state
  • connection validation still rejects invalid edges
  • drag-and-drop creation still supports both node payloads and file uploads

Test shape

  • Extract deterministic logic from hook internals into pure helpers where possible.
  • Add targeted tests for those helpers first.
  • Add hook-level tests only for behavior that genuinely depends on refs, effects, or queue timing.
  • Keep manual verification for React Flow pointer interactions that are awkward to simulate precisely.

Refactor Sequencing

Use small, behavior-preserving phases.

Phase 1: Sync engine extraction

  • Highest complexity
  • Highest test ROI
  • Unlocks simpler downstream hooks because many callbacks become imported capabilities instead of locally defined machinery

Phase 2: Flow reconciliation extraction

  • Separate mapping and merge behavior from interaction code
  • Makes the render path easier to inspect

Phase 3: Connections and drop extraction

  • Split creation pathways by intent: connect vs drop
  • Reduces the size of the lower half of canvas.tsx

Phase 4: Optional presentational view extraction

  • Only if the remaining orchestrator still feels too dense

Guardrails

  • Do not change Canvas or CanvasInner public props.
  • Keep ReactFlowProvider and useReactFlow usage exactly compatible with current behavior.
  • Keep nodeTypes outside React components.
  • Preserve ordering of side effects and queue mutations.
  • Do not introduce new dependencies.
  • Do not rewrite into a reducer architecture in this phase.

Expected Outcome

  • components/canvas/canvas.tsx becomes an understandable composition root instead of a monolith.
  • The highest-risk behaviors remain intact because the refactor follows existing seams rather than inventing a new runtime model.
  • A second engineer can work in the canvas codebase by area of concern instead of first learning the entire file.