Skip to content

MindMapState Normalization Refactor

Overview

We refactored the MindMapState to use a normalized data structure for storing mind map nodes. Previously, nodes were stored in a flat record, but parent-child relationships were only defined by a parentId on the child. We have now added an explicit children array to each node.

Concept: What is a Normalized Data Structure?

In the context of client-side state management (like Redux or React Context), normalization refers to structuring data similarly to a relational database to avoid duplication and ensure consistency.

Key Characteristics

  1. Flat Structure: Instead of nesting objects deeply (e.g., Node -> Children -> Children), every entity is stored at the top level in a dictionary/map keyed by its ID.
  2. References by ID: Relationships between entities are stored as IDs (e.g., children: ['id1', 'id2']) rather than embedding the actual objects.

Connection to Database Normalization

The term is directly borrowed from Database Normalization theory. In a relational database, normalization is the process of organizing data to reduce redundancy and improve data integrity. The parallels are exact:

  1. Primary Keys: In our state, nodes: Record<string, Node> acts like a database table where the id is the Primary Key.
  2. Foreign Keys: When we store children: ['child1', 'child2'], we are storing Foreign Keys that point to other records in the "table". We are not embedding the actual child objects.
  3. Single Source of Truth: Just like in a normalized database, each piece of data exists in exactly one row. This prevents Update Anomalies—where you update a record in one place but forget to update a copy of it elsewhere.

So, when frontend developers talk about "normalized state," they are consciously applying these database design principles to JavaScript objects to solve the exact same problems (consistency and efficient updates).

"Normalization" vs. "Denormalization" in this Refactor

Technically, our specific implementation is a hybrid:

  • Normalized: We keep the flat nodes object keyed by ID.
  • Denormalized (Optimization): We store the relationship in two places: the child has a parentId, and the parent has a children array.
    • This "denormalization" (redundancy) is intentional. It trades a small amount of memory and update complexity (updating both sides on change) for massive performance gains in traversal speed (O(1) access in both directions).

Changes

  • Data Structure: Added children: string[] to MindMapNodeData.
  • State Management: Updated mindMapReducer to maintain the children array during ADD_NODE, DELETE_NODES, and MOVE_NODES operations.
  • Component Logic: Updated Studio.tsx and helper functions to use node.children for traversal instead of iterating over the entire node list.

Key Benefits

1. Performance (O(1) vs O(N))

  • Child Lookup: Previously, finding children of a node required filtering the entire nodes object (O(N)). Now, we can directly access node.children (O(1)).
  • Rendering Loop: We eliminated an O(N) check inside the main rendering loop (checking if a node has children), reducing the complexity of rendering the canvas from O(N²) to O(N).

2. Scalability

  • As the number of nodes grows into the thousands, operations like "Collapse Branch", "Select Descendants", and "Delete Subtree" remain instant because they no longer need to scan unrelated nodes.

3. Maintainability

  • Explicit Relationships: The tree structure is now explicitly defined in the state, making it easier to debug and reason about the graph topology.
  • Standard Pattern: This aligns with the "normalized state" pattern commonly used in Redux and other state management libraries, making the codebase more familiar to other developers.

Example Comparison

Before (Denormalized / Parent Pointer Only):

typescript
// Finding children of 'nodeA'
const children = Object.values(nodes).filter(n => n.parentId === 'nodeA'); // Scans ALL nodes

After (Normalized / Doubly Linked):

typescript
// Finding children of 'nodeA'
const childrenIds = nodes['nodeA'].children; // Instant access
const children = childrenIds.map(id => nodes[id]);