lundie.io Get In Touch

Phase 4: Tree Component Birth

2–28 August 2025

Building the Tree

This phase produced NestedSortableTree.tsx, the component that ended up handling the entire creative graph UI. It started simple. It didn't stay that way – and I was uncomfortable with how heavy it became. But refactoring would have taken significant time away from exploration at a stage where the app was still being shaped. The technical debt was accepted deliberately.

The First Tree

Commit: 592ac7a – "Enhanced Tree Component with Insertion Line Drop Targets"

The initial implementation used @dnd-kit for drag-and-drop, with insertion line indicators showing exactly where a dragged node would land:

  • Insertion line drop targets: Blue visual indicators showing where items would land
  • Before/after positioning: Drop above or below existing nodes
  • Depth awareness: Indentation showing hierarchy level
  • Animated pulse effects: Visual feedback during drag operations
  • Collision detection: Changed from closestCenter to pointerWithin for precision
Early Tree Structure
1
2
3
4
5
6
7
ThoughtNode "Project Ideas"
  ├─ before indicator
  ├─ PromptNode "What if...?"
  │  ├─ InsightNode "Interesting connection"
  │  └─ after indicator
  ├─ PromptNode "Consider this..."
  └─ after indicator

Node Capabilities System

Commit: f0b6e46 – "Implement Node Capabilities System with BaseNode Refactor"

Not all node types can have all children – and that was a deliberate UX decision, not just a data model constraint. The goal was enough structure to guide the user through idea-chaining without so many options that choosing the next node type became its own decision. The capabilities existed informally before this commit; the refactor gave them a proper type system and a single source of truth in DEFAULT_NODE_CAPABILITIES. Kept frontend-only by design – the API could mirror it later, but keeping it in the UI allowed experimentation without locking in the schema.

Default Node Capabilities
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export const DEFAULT_NODE_CAPABILITIES: NodeCapabilityConfig = {
  thought: {
    canHaveChildren: true,
    allowedChildTypes: ['prompt', 'insight', 'result'],
    allowedActions: ['add_child', 'edit', 'delete', 'duplicate'],
    canBeEdited: true,
    canBeDeleted: true,
  },
  prompt: {
    canHaveChildren: true,
    allowedChildTypes: ['insight', 'result'],
    allowedActions: ['add_child', 'favorite', 'delete', 'duplicate'],
    canBeEdited: false,
    canBeDeleted: true,
  },
  insight: {
    canHaveChildren: true,
    allowedChildTypes: ['prompt', 'insight', 'result'],
    allowedActions: ['add_child', 'edit', 'delete', 'duplicate'],
    canBeEdited: true,
    canBeDeleted: true,
  },
  result: {
    canHaveChildren: false,
    allowedChildTypes: [],
    allowedActions: ['edit', 'delete', 'duplicate'],
    canBeEdited: true,
    canBeDeleted: true,
  },
};

The shape of this changed considerably by Phase 9 – edge rules and additional node types were added as the graph model matured.

Feature-Based Restructure

Commit: 8cce63f – "Restructured to feature/domain based directory and file layout"

As the tree grew more complex, the codebase was reorganised:

New Directory Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
src/
├── features/
│   ├── editor/            # Node editing forms
│   ├── nodes/             # Node component library
│   │   ├── components/    # ThoughtNode, PromptNode, InsightNode, ResultNode, BaseNode
│   │   ├── types/
│   │   └── utils/
│   └── tree/              # The tree component
│       ├── components/    # NestedSortableTree, SortableTreeNode, InsertionIndicator, NodeDropTarget
│       ├── services/      # NodeWorkflowManager
│       └── utils/         # TreeDataAdapter
└── shared/
    ├── api/
    └── types/

Graph Integration & the Edge Manager

Commits: 4b7d3d6, eeee0d3

The tree needed to sync with the backend's edge system. Two managers were created:

NodeWorkflowManager

Type-safe tree operations:

  • addNode(): insert new node with validation
  • moveNode(): reorder or reparent nodes
  • removeNode(): delete with cascade handling
  • updateNode(): modify node content
  • findNodeById(): recursive tree search
  • wouldCreateCircularReference(): prevent infinite loops

EdgeManager (later removed)

A separate EdgeManager handled edge creation/deletion initially. It was removed because strict SOC was getting in the way of actually writing the app – the boundary between tree mutations and edge mutations was too blurry to enforce cleanly without the full architectural picture. Not my usual approach, but I believed it was the right call at this stage. The edges were folded back into the mutation pipeline and properly addressed in Phase 6.

API Integration

Commit: 8c8ea27 – "Set up API integration infrastructure and restructure TreeTest page"

The tree component was connected to the FastAPI backend. Drag operations now synced with Firestore rather than just updating local state.

Optimistic updates weren't really a choice – waiting for a server round-trip on every drag or node creation would break the flow the app was built around. The UI updated immediately; the API caught up in the background. That decision defined the next several weeks of work.

Optimistic Updates & Mutation Queue

Commits: b2fd496, c32d085

Creating a node optimistically meant giving it a client-side temp_abc123 ID immediately, inserting it into the tree, then replacing that with the real Firestore ID once the API responded – recursively across the whole tree:

TempId Replacement (Recursive)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const replaceNodeIds = (items: TreeItemData[]): TreeItemData[] => {
  return items.map(item => {
    const newId = mappings.get(item.id);
    const updatedItem = newId
      ? { ...item, id: newId, nodeData: { ...item.nodeData, id: newId } }
      : item;

    if (updatedItem.children && updatedItem.children.length > 0) {
      return {
        ...updatedItem,
        children: replaceNodeIds(updatedItem.children)  // Recurse!
      };
    }

    return updatedItem;
  });
};

Mutation Queue

A debounced queue prevented API spam during rapid drag operations – local state updated immediately, deltas synced after 200ms of inactivity, sending only changed edges rather than the full tree.

Batched Deletion & Tombstones

Commits: 4eb16dd, b3b89b1

Deleting nodes optimistically had its own problem: what if the server rejected the delete? The tombstone pattern solved it – a node was added to a pendingDeletions[] array and hidden from the UI immediately, then the delete was sent to the API after 200ms. On success it was removed from the tree; on failure it was quietly restored. No flicker.

The 23 Hooks

By end of August the component worked. But 23 hooks in a single component felt like a code smell – I was new enough to React that I couldn't fully articulate why, just that the quantity felt off. The more immediate problem was a ~1 second drag activation lag. Phase 5 was about that.

Hook Breakdown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// State Management
useState × 5    (treeItems, activeId, pendingNodes, pendingDeletions, hasOptimisticUpdates)

// Refs
useRef × 5      (previousTreeRef, debounceTimerRef, lastCreatedNodeIdRef,
                 onNodesChangeRef, onEdgesChangeRef)

// Memoization
useMemo × 4     (sortableIds, filteredTree, updateTreeMutations, dndSensors)

// Callbacks
useCallback × 6 (handleDragStart, handleDragEnd, onNodeCreate, onNodeDelete,
                 fetchAISuggestions, applyTombstones)

// Effects
useEffect × 3   (sync from props, cleanup timers, reconcile server state)

Key Commits from This Phase

Web 592ac7a 2025-08-02
Enhanced Tree Component with Insertion Line Drop Targets
Web f0b6e46 2025-08-04
[Full Refactor: Nodes] Implement Node Capabilities System with BaseNode Refactor
Web 8cce63f 2025-08-05
Restructured to feature/domain based directory and file layout
Web 4b7d3d6 2025-08-06
[Implementation] Frontend Graph Integration for Edge System
Web eeee0d3 2025-08-07
[Refactor] Split NodeWorkflowManager into separate tree and edge managers
Web 8c8ea27 2025-08-13
[Feat](api, tree): Set up API integration infrastructure
Web 1bfdcc5 2025-08-20
[Refactor] Unified node type system and remove redundant conversions
Web b2fd496 2025-08-21
[Fix/Refactor] Node creation, API integration and optimistic updates
Web c32d085 2025-08-22
[Feat/Refactor] Tree mutation queue, toasts and bug fixes
Web 4eb16dd 2025-08-25
[Feat] Batched node deletion with staged UI and ActionButtons
Web b3b89b1 2025-08-28
[Feat] Batched node deletion with staged workflow and ActionButtons

Get In Touch

Prefer using email? Say hi at hello@lundie.io