Skip to main content
  1. Yjs Internals/

Chapter 2: YType #

In Chapter 1, we learned that Doc is like a collaborative notebook. We created a Y.Text inside it and synced two Docs with updates. But a notebook is only useful when you can write different kinds of things in it: lists, notes, settings, and more. In Yjs, those different kinds of things are called YTypes.


What problem does YType solve? #

Imagine you and your roommate want to share a grocery list. You both need to add items, remove items, and see each other’s changes. If you use a normal JavaScript array, it looks like this:

const list = ['milk']
list.push('eggs')

This is fine for one person, but it’s just a local array. It has no memory of who changed what, and it definitely isn’t shared with anyone. If your roommate does the same thing on their phone, the two lists will never meet.

A YType fixes that problem. It is a shared data type that lives inside a Doc, and it can:

  • capture every change automatically
  • let you observe changes
  • be merged with other copies through updates

So the same grocery list becomes something you and your roommate can edit together.


Meet the YType family #

YType is not one single class. It’s a family of shared data types.

YTypeFeels like a JavaScript…Use it for
Y.ArrayArrayordered lists
Y.MapMapkey-value pairs
Y.TextStringshared text
Y.XmlElementDOM elementstructured XML/HTML content

All of them look and behave a little like normal JavaScript data structures, but they have Yjs superpowers.

Here’s a quick taste of each one.

Y.Array #

const doc = new Y.Doc()
const list = doc.getArray('grocery')
list.push(['milk', 'eggs'])
console.log(list.toArray()) // ['milk', 'eggs']

A Y.Array is like a normal array. One important difference: push expects an array, because you can insert several items at once.

Y.Map #

const profile = doc.getMap('profile')
profile.set('name', 'Ada')
console.log(profile.get('name')) // 'Ada'

A Y.Map is like a Map for key-value data. You can use it for user profiles, settings, or any structured object.

Y.Text #

const note = doc.getText('note')
note.insert(0, 'Hello Yjs!')
console.log(note.toString()) // 'Hello Yjs!'

A Y.Text is shared text. You can insert text at a position, and later convert it back to a normal string.

Y.XmlElement #

const page = doc.getXmlElement('page')
page.setAttribute('theme', 'dark')

A Y.XmlElement is for shared XML/HTML-like data. You don’t need it on your first day, but it’s good to know it exists.


The three superpowers of a YType #

Let’s look at the grocery list example to see these superpowers in action.

Superpower 1: Changes are captured #

Every time you change a YType, Yjs remembers exactly what happened.

const doc = new Y.Doc()
const grocery = doc.getArray('grocery')

grocery.push(['milk'])
grocery.push(['eggs', 'bread'])

console.log(grocery.toArray()) // ['milk', 'eggs', 'bread']

Yjs doesn’t just update the array. It stores tiny pieces of change information inside the Doc. Later, those pieces can be turned into an update and sent to someone else.

Superpower 2: Changes can be observed #

You can listen for changes and react to them.

grocery.observe(() => {
  console.log('The list now contains:', grocery.toArray())
})

grocery.push(['cheese'])
// "The list now contains: ['milk', 'eggs', 'bread', 'cheese']"

The observe function runs every time the YType changes, whether the change came from you or from a remote friend.

Superpower 3: Changes can be merged #

The real magic of Yjs is that changes from different people can be combined safely.

const update = Y.encodeStateAsUpdate(doc)

const friendDoc = new Y.Doc()
Y.applyUpdate(friendDoc, update)

console.log(friendDoc.getArray('grocery').toArray())
// ['milk', 'eggs', 'bread', 'cheese']

The update contains everything your friend needs to rebuild the grocery list. You don’t send the whole list as a plain string. You send an encoded update that Yjs can apply to another Doc.


How a YType works under the hood #

Let’s see what happens when you push "milk" to a Y.Array.

Imagine you are adding a sticky note to a shared whiteboard. You don’t just put it anywhere. You tell the whiteboard: “I am adding this note at the end, and I am the person who added it.” The whiteboard keeps a record of that note. Later, someone else can see the same note and know where it belongs.

Here is a simple picture of that process:

sequenceDiagram participant You participant YArray participant Doc participant Store as StructStore You->>YArray: push("milk") YArray->>Doc: start a transaction Doc->>Store: create and store an Item Store-->>Doc: Item stored Doc-->>YArray: transaction finished YArray-->>You: observe event fires

Step by step:

  1. You call grocery.push(['milk']).
  2. The YArray tells the Doc: “I want to make a change.”
  3. The Doc starts a transaction. A transaction is a short-lived workspace for the change. You’ll learn more in Transaction.
  4. Inside the transaction, Yjs creates a small struct called an Item that holds the content "milk".
  5. The Item is stored in the Doc’s StructStore, which is the warehouse for all changes.
  6. The transaction finishes.
  7. Your observe callback runs.

A look at the source code #

The real Yjs code is more complex, but the idea is simple. Let’s look at a very simplified version.

The base YType class is small. All shared types inherit from it.

// simplified from src/ytype.js
class YType {
  constructor(doc) {
    this.doc = doc
  }
}

A YArray extends YType and adds array-like methods.

// simplified from src/types/YArray.js
class YArray extends YType {
  push(content) {
    this.doc.transact(() => {
      // create Items and add them to the array's internal list
    })
  }
}

When you push content, Yjs creates an Item. An Item is a struct that stores the content and its position in the list.

// simplified from src/structs/Item.js
class Item {
  constructor(id, content) {
    this.id = id       // who created this item and where
    this.content = content
    this.left = null   // the item before this one
    this.right = null  // the item after this one
  }
}

For a Y.Array, these items are linked together like a chain. For a Y.Map, Yjs uses a different internal layout. For a Y.Text, the items contain pieces of text and formatting.

You don’t need to memorize all of this. But knowing that Item and StructStore exist helps you understand the next chapters.


“YType” as a name #

In the Yjs TypeScript declarations, you might see YType listed as a type. That’s just Yjs’s way of saying “any shared type.”

declare type YType = import('./src/ytype.js').YType

When you see YType in the source code, think of the whole family: Y.Array, Y.Map, Y.Text, and Y.XmlElement.


Conclusion #

You now know the most important idea in Yjs: a YType is a shared data type that behaves like a normal JavaScript data structure, but with superpowers.

  • A YType lives inside a Doc.
  • The main types are Y.Array, Y.Map, Y.Text, and Y.XmlElement.
  • Every change is captured by the Doc.
  • You can observe changes with observe.
  • Changes can be encoded into updates and applied to other Docs.
  • Internally, a YType is made of structs called Items.

So the notebook from Chapter 1 now has pages you can write on. But what exactly happens when you make a change? That’s the subject of the next chapter.

Continue to: Transaction


Generated by AI Codebase Knowledge Builder