Why It Matters
Prevents out-of-sync states (like nodes with missing parent indices or invalid edge keys) by forcing all layout and node operations to be processed as transaction payloads.
mindmap.build / react architecture
This architectural catalog outlines the core React patterns, custom hooks, dynamic layout strategies, and performance shortcuts that keep our infinite mind mapping studio canvas smooth and responsive.
A single source of truth for the entire mindmap coordinate grid. Rather than orchestrating dozens of disjointed useState and useEffect hooks, mindmap.build uses a central reducer to manage nodes, links, selection, layout configurations, and historical stacks.
Prevents out-of-sync states (like nodes with missing parent indices or invalid edge keys) by forcing all layout and node operations to be processed as transaction payloads.
export const mindMapReducer = (state, action) => {
switch (action.type) {
case 'LOAD_GRAPH':
return handleLoadGraph(state, action.payload)
case 'REPARENT_NODE':
return handleReparentNode(state, action.payload)
case 'MOVE_NODES':
return handleMoveNodes(state, action.payload)
case 'UNDO':
return handleUndo(state)
default:
return state
}
}mindmap.build passes event handlers down to dozens of interactive mindmap nodes. To prevent layout thrashing and unnecessary renders, these callback functions must remain stable. We bypass the React rendering tree by updating a mutable handlersRef inside useLayoutEffect, passing down standard, immutable wrapper callbacks.
Without this ref bypass, every keystroke in a node editor would re-allocate new callback instances, causing all sibling nodes on the infinite canvas to execute reconciliation, leading to typing lag.
const handlersRef = useRef({
onMouseDown: handleNodeMouseDown,
onUpdateText: updateNodeText,
})
useLayoutEffect(() => {
handlersRef.current = {
onMouseDown: handleNodeMouseDown,
onUpdateText: updateNodeText,
}
}, [handleNodeMouseDown, updateNodeText])
const stableCallbacks = useMemo(
() => ({
onMouseDown: (e, id) => handlersRef.current.onMouseDown(e, id),
onUpdateText: (id, txt) => handlersRef.current.onUpdateText(id, txt),
}),
[]
)Infinite canvas apps represent multiple complex subsystems running concurrently. mindmap.build isolates these concerns into specialized custom hooks that coordinate with the central state and UI ref pointers.
Maintains clean code isolation. Viewport matrices, drag-reparent mathematics, keyboard event handlers, and data syncing do not bleed into the render layers.
// Hooks Entry point index.ts
export { useStudioViewport } from './useStudioViewport'
export { useStudioCanvasInteractions } from './useStudioCanvasInteractions'
export { useStudioCommands } from './useStudioCommands'
export { useStudioKeyboardShortcuts } from './useStudioKeyboardShortcuts'
export { useStudioRenderModel } from './useStudioRenderModel' Mindmap layout algorithms (like tree balancing) cannot calculate node positions without knowing the actual text wrap dimensions. mindmap.build uses ResizeObserver on node elements to capture exact dimensions and sync them back to the state container.
Resolves the "measure-render" loop hazard. By using requestAnimationFrame and checking dimension differentials, mindmap.build breaks layout calculation cycles that cause page freezes.
const observer = new ResizeObserver((entries) => {
const entry = entries.find((e) => e.target === element)
const dimensions = measureStudioNodeElement(element, entry)
if (dimensions) {
onMeasureNode(node.id, dimensions)
}
})
observer.observe(element, { box: 'border-box' })Standard HTML textareas do not grow vertically to match typing height. mindmap.build layers an invisible copy of the node text (a ghost wrapper) directly beneath the active textarea input within a single grid cell.
Eliminates layout lag. Using pure CSS grids allows the browser to perform calculations during the layout reflow pipeline, keeping frame rates at 60fps.
<div className="grid place-items-center w-full relative">
{/* GHOST WRAPPER: Grows grid cell organically */}
<div className="col-start-1 row-start-1 invisible whitespace-pre-wrap break-word">
{displayText || '\u00A0'}
</div>
{/* ACTIVE INPUT: Stretches to fill container bounds */}
<RichTextNodeEditor className="col-start-1 row-start-1 absolute inset-0 w-full h-full" />
</div>Mindmap canvases use heavy styling transforms like translate and scale. Any child menus, styles lists, or tools positioned inside them will be cut off by layout masks. Radix UI primitives use React Portals to break elements out of these layouts.
Guarantees float widgets and overlay dropdown panels maintain correct positions and pixel-perfect sizes without getting clipped by canvas boundaries.
import { createPortal } from 'react-dom'
export function PortalOverlay({ children }) {
// Pulls overlay content out of canvas context
// and mounts it directly into document.body
return createPortal(
<div className="absolute z-50 float-menu">
{children}
</div>,
document.body
)
} Re-rendering hundreds of canvas components during panning causes layout latency. mindmap.build prevents useless reconciliations by wrapping nodes in React.memo and moving positions using GPU compositing transforms.
Shifts node coordinates directly to the GPU compositor via composite layering transforms, bypassing standard CPU-intensive render trees.
export const MindMapNodeComponent = React.memo(
(props) => {
// Renders node contents...
},
(prevProps, nextProps) => {
// Custom equality checks to avoid re-rendering
return (
prevProps.isSelected === nextProps.isSelected &&
prevProps.isEditing === nextProps.isEditing &&
prevProps.node.x === nextProps.node.x &&
prevProps.node.y === nextProps.node.y &&
prevProps.node.text === nextProps.node.text
)
}
)Mutating multiple coordinates concurrently (e.g., resizing nodes or multi-selection stylings) creates a history footprint. We isolate transformations into transactional boundaries using batch markers.
Ensures that undoing a drag operation moves the entire group of nodes back to their starting positions collectively, rather than reversing one pixel coordinate at a time.
case 'BEGIN_BATCH_UPDATE':
return {
...state,
history: [...state.history, createHistorySnapshot(state)],
future: [],
}
case 'COMMIT_BATCH_UPDATE':
// Finishes batch sequence and solidifies state
return state;