Chapter 1: Doc #
Welcome! In Yjs, everything starts with the Doc. It is the top-level container for all shared data. Think of it as a collaborative notebook that lives on your device. You can put shared types inside it, edit them, and later send those updates to other devices.
What problem does Doc solve? #
Imagine Alice and Bob are writing a shared note together. Alice is on her laptop, Bob is on his phone. They are not always online, but when they reconnect, their notes should become the same.
Without something like Yjs, you would need to solve hard problems by yourself:
- How do I represent the note on my device before syncing?
- How do I merge my changes with Bob’s changes?
- How do I avoid losing text if we edit the same sentence at the same time?
Doc is Yjs’s answer to the first problem. It gives you a local object that holds all shared data and tracks every change. Later, Yjs can turn those changes into updates that other Doc instances can apply.
A Doc is not a server, and it is not a database. It is more like a local notebook that knows how to merge with other notebooks.
A Doc is like a collaborative notebook #
Imagine a physical notebook with different sections:
- One section for plain text notes
- One section for lists
- One section for key-value pairs
In Yjs, those sections are called shared types. The Doc is the notebook itself. You can have many notebooks, and each one can contain many shared types.
Here is the simplest possible Yjs program:
import * as Y from 'yjs'
const doc = new Y.Doc()
That creates a new, empty collaborative notebook.
Adding shared types to a Doc #
You usually don’t put raw JavaScript values directly into a Doc. Instead, you create a Yjs shared type. The most common types are:
Y.Textfor textY.Mapfor key-value dataY.Arrayfor ordered lists
We will talk much more about shared types in the next chapter. For now, let’s add a Y.Text type to our Doc.
const text = doc.getText('note')
text.insert(0, 'Hello Yjs!')
console.log(text.toString()) // "Hello Yjs!"
What just happened?
doc.getText('note')asks theDocfor a shared text type namednote.- If that type does not exist yet, the
Doccreates it. text.insert(0, 'Hello Yjs!')inserts text at position 0.
If you call doc.getText('note') again, you get back the exact same shared text type.
const again = doc.getText('note')
console.log(again === text) // true
This is important: names are like labels in your notebook. If you use the same label, you get the same shared type.
Every Doc has a unique client ID #
A Doc also has a clientID, which is a random number that identifies this local notebook. It helps Yjs know which device created which change.
const doc = new Y.Doc()
console.log(doc.clientID) // a random number, e.g. 734211
Do not worry too much about this yet. Just know that every Doc has its own identity.
Syncing two Docs with updates #
Now here is the fun part: we can synchronize two separate Doc instances.
Suppose we have Alice’s Doc. Then Bob has his own empty Doc.
const update = Y.encodeStateAsUpdate(doc)
update is a compact binary Uint8Array that represents the state of Alice’s Doc. It is like a postcard that contains all the changes Bob needs.
Now Bob can apply it to his own Doc.
const remoteDoc = new Y.Doc()
Y.applyUpdate(remoteDoc, update)
console.log(remoteDoc.getText('note').toString()) // "Hello Yjs!"
Bob’s Doc now contains the same text. We did not send the actual text string directly. Instead, we encoded the state of the whole Doc into an update, then applied that update to another Doc.
This is how Yjs can work over any network. You send updates, not whole documents.
Listening for changes #
Usually, you don’t want to encode the whole document every time. Instead, you can listen to the update event on a Doc.
doc.on('update', (update, origin, doc) => {
sendToFriend(update)
})
When you change a shared type, the Doc automatically creates an update and emits it. The update is a Uint8Array that you can send to other peers. When they receive it, they can apply it with Y.applyUpdate.
This is how a simple collaborative app is built:
- Alice makes a change.
- Alice’s
Docemits anupdate. - The update is sent to Bob.
- Bob’s
Docapplies the update. - Bob’s screen updates.
What’s under the hood? #
Let’s look at what happens inside the Doc when you make a change.
Here is a simple picture of Alice and Bob syncing:
Now let’s translate that into simple, code-level thinking.
When you call doc.getText('note'), the Doc keeps a map of shared types. Inside the real Yjs source at src/utils/Doc.js, it looks something like this:
// simplified from src/utils/Doc.js
getText(name) {
if (!this.share.has(name)) {
this.share.set(name, new YText(this, name))
}
return this.share.get(name)
}
That is why the same name gives you the same shared type. The Doc is remembering what is inside the notebook.
When you insert text, Yjs doesn’t just store a simple string. It creates small pieces of data called structs. All those structs are kept in the Doc’s StructStore. You can think of the StructStore as a warehouse where every changed piece of the notebook is carefully stored in order.
// simplified from src/utils/Doc.js
class Doc {
constructor() {
this.clientID = randomClientID()
this.share = new Map()
this.store = new StructStore()
}
}
Here:
clientIDtells the warehouse which device created each item.shareis the map of shared types.storeis the warehouse of all structs.
When you edit a shared type, Yjs opens something called a Transaction. The transaction gathers all changes, updates the warehouse, and produces an update.
// simplified from src/utils/Transaction.js
const transaction = new Transaction(doc)
// all changes are applied inside this transaction
transaction.finish()
We will cover transactions in
Transaction. For now, think of a transaction as a short-lived workspace where the Doc records what changed.
The update you send is just an encoded version of the structs that were added or changed. The receiving Doc decodes that update and adds the structs to its own warehouse. This is why two Doc instances converge to the same content.
A tiny TypeScript note #
In the Yjs source code, Doc is also declared as a type in global.d.ts:
declare type Doc = import('./src/utils/Doc.js').Doc
This is mainly there to give TypeScript and editors useful information about the Doc class. You don’t need to understand it to use Yjs.
Common beginner mistakes #
Don’t create a new
Docfor every keystroke.
Use oneDocper local peer. Reuse it.Don’t send the
Docobject over the network.
You can send updates, which are binaryUint8Arrayobjects.Don’t expect two different
Docinstances to share data automatically.
You must apply updates to synchronize them.
The Doc is your local copy of the collaborative state. Updates are how different copies talk to each other.
Conclusion #
You learned the most important piece of Yjs: the Doc.
- A
Docis a top-level container for all shared data. - It can hold shared types like
Y.Text. - It has a unique
clientID. - It tracks every change and produces updates.
- Updates can be applied to other
Docinstances to sync them. - Internally, the
Docuses aStructStoreand transactions to manage changes.
Now that you have a notebook, it is time to look at what you can put inside it: shared types.
Next up: YType
Generated by AI Codebase Knowledge Builder