Container
Containers are the fundamental building blocks in Loro for organizing and structuring collaborative data. They provide typed data structures that automatically merge when concurrent edits occur.
Container Types
Loro provides several container types, each optimized for different use cases:
- LoroMap: Key-value pairs with Last-Write-Wins semantics
- LoroList: Ordered sequences that merge concurrent insertions
- LoroText: Text with character-level merging and rich text support
- LoroTree: Hierarchical tree structures with move operations
- LoroMovableList: Lists with reordering capabilities
- LoroCounter: Numerical values with increment/decrement operations
Container States: Attached vs Detached
Containers in Loro exist in two distinct states that affect their behavior and identity.
Detached Containers
A container is detached when created directly using constructors:
// These containers are all detached
const = new ();
const = new ();
const = new ();Characteristics of detached containers:
- Not yet part of any document
- Have a default placeholder ContainerID
- Can be used as templates or temporary data structures
- Will get a proper ContainerID when inserted into a document
Attached Containers
A container becomes attached when it’s part of a document hierarchy:
const = new ();
// Root containers are immediately attached
const = .("myMap");
const = .("myText");
// Child containers: the returned value is attached
const = new ();
const = .("child", );
// Note: detachedChild remains detached
// attachedChild is the attached version with proper ContainerIDCharacteristics of attached containers:
- Belong to a specific document
- Have a proper ContainerID that uniquely identifies them
- Changes are tracked in the document’s history
- Can be synchronized across peers
Container IDs
Every attached container has a unique ContainerID that identifies it within the distributed system. The ID generation depends on the container type:
- Root containers: ID derived from their name (e.g., “myMap” in
doc.getMap("myMap")) - Child containers: ID based on the operation that created them (OpID)
This deterministic ID generation ensures that:
- The same container can be identified across all peers
- Container IDs are not random but contextually determined
- A detached container cannot have its final ID until insertion
Working with Containers
Creating Root Containers
Root containers are created through the document API and are immediately attached:
const = new ();
// These methods create or get root containers
const = .("settings");
const = .("content");
const = .("items");
const = .("hierarchy");Nesting Containers
Containers can be nested to create complex data structures:
const = new ();
const = .("root");
// Method 1: Using setContainer (returns attached container)
const = .("description", new ());
// Method 2: Using insertContainer for lists
const = .("items");
const = .(0, new ());Container Overwrites
When initializing child containers in parallel, overwrites can occur instead of automatic merging. For example:
const : string = "hello";
const = new ();
const = .("map");
// Parallel initialization of child containers
const = .();
const = .("map").("text", new ());
.(0, "A");
const = .("map").("text", new ());
.(0, "B");
.(.({ : "update" }));
// Result: Either { "meta": { "text": "A" } } or { "meta": { "text": "B" } }This behavior poses a significant risk of data loss if the editing history is not preserved. Even when the complete history is available and allows for data recovery, the recovery process can be complex.
When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues.
Mergeable Containers
The overwrite happens because setContainer creates a regular child container.
Its Container ID includes the operation that created it, so two peers that
create map["text"] concurrently create two different Text containers.
When a child container should be identified by its logical position in a Map, use a mergeable child container instead:
const text = doc.getMap("map").ensureMergeableText("text");
text.insert(0, "A");Peers that call ensureMergeableText("text") on the same parent Map address the
same logical Text container. The same pattern is available for Map, List,
MovableList, Tree, and Counter children through the ensureMergeable* methods.
Best Practices
-
For dynamic Map keys where peers may lazily create the same child, use
ensureMergeableText,ensureMergeableMap,ensureMergeableList, and the otherensureMergeable*methods. -
When the structure is fixed and known ahead of time:
- If possible, initialize all child containers during the map container’s initialization
-
Use regular
setContainer/insertContainerwhen each creation should produce a distinct child object, or when you are modeling replacement rather than shared initialization. -
A unique root container name can still be a good fit when the child naturally belongs at the root, for example
doc.getMap("user." + userId).
Related Concepts
- Container ID: Deep dive into how Container IDs work
- Choosing CRDT Types: Guide for selecting the right container type
- Composition: How to compose containers into complex structures