ID-Based Storage

The most important pattern for collaborative applications: storing content by ID and referencing by ID.

Overview

ID-based storage separates what content exists from where it appears. Store content in a map keyed by unique IDs, and reference those IDs from elsewhere.

This prevents data loss during concurrent reordering and enables reliable cross-references.

The Problem

Storing content directly in a reorderable array:

// WRONG - content in array
const slides = yDoc.getArray('slides')
slides.push([{ id: 'k8d2fn3m', title: 'Intro', content: [...] }])

When two users reorder the same slide concurrently, delete + insert creates copies—resulting in duplicated or lost content.

The Solution

Separate content storage from ordering:

const slideContent = yDoc.getMap('slides')
const slideOrder = yDoc.getArray('slideOrder')

// Store content by ID
slideContent.set('k8d2fn3m', { title: 'Intro', content: [...] })

// Store only ID in ordering array
slideOrder.push(['k8d2fn3m'])

Now reordering only moves IDs—lightweight operations that merge cleanly.

Core Operations

All operations should be wrapped in yDoc.transact() for atomicity:

Operation Steps
Create content.set(id, data) + order.push([id])
Reorder Delete ID from old index, insert at new index
Delete order.delete(index, 1) + content.delete(id)
Read order.toArray().map(id => content.get(id))

Always delete from both order and content to avoid orphaned data.

Benefits

  • Safe Reordering: Moving items only moves IDs, which merge cleanly
  • Stable References: Other parts can reference by ID without breaking
  • Efficient Updates: Content changes don’t affect order, and vice versa
  • Easy Deletion: References become stale IDs that can be filtered
  • Undo Granularity: Content and order changes are separate undo steps

Hierarchical Data (Trees)

ID-based storage extends naturally to trees. Store all nodes flat with parent references:

nodes/
├── k8d2fn3m → { name: 'Documents', parentId: null }
├── m4x9pt2q → { name: 'Work', parentId: 'k8d2fn3m' }
├── j7n3ks8w → { name: 'Report.md', parentId: 'm4x9pt2q' }
└── p2r6vm4c → { name: 'Notes.md', parentId: 'm4x9pt2q' }

Why flat beats nested maps for trees:

  • Moving nodes is a single parentId update, not delete + insert
  • Cross-references (shortcuts, symlinks) work naturally
  • Depth changes don’t require restructuring

For sibling ordering, add a childOrder array per parent or a separate ordering map.

Common Mistakes

Mistake Problem Solution
Using array indices as references Indices shift when items are inserted/deleted Use stable IDs for references
Deleting from order only Orphaned content accumulates in the map Delete from both order and content
Not using transactions Sync may occur between operations Wrap related changes in yDoc.transact()

See Also