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
parentIdupdate, 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
- Ordering with Arrays - More on Y.Array for ordering
- Transactions - Batching related changes