Block Reactive
In BlockSuite, we utilize yjs as our underlying technology. However, we don't expect every developer to have an in-depth understanding of yjs before working with BlockSuite. To make things easier, we've created a reactive layer that allows block models to use native JavaScript data types. Still, you might be curious about how your data is managed behind the scenes—so let's take a closer look.
Access the Yjs Data
If you want to inspect the data in Yjs directly, you can access it from yBlock
in your block model:
model.yBlock.get('prop:foo'); // 0
model.yBlock.get('prop:bar'); // "hello"
Proxy, Yjs and Signal
For each property in a model, we create a proxy and a signal to simplify usage:
defineBlockSchema({
// ...
props: () => ({
count: 0
}),
})
blockModel.yBlock.get('prop:count') // 0
blockModel.props.count // 0
blockModel.props.count$ // { value: 0 }
// update with proxy
blockModel.props.count = 1;
blockModel.yBlock.get('prop:count') // 1
blockModel.props.count$ // { value: 1 }
// update with signal
blockModel.props.count$.value = 2;
blockModel.yBlock.get('prop:count') // 2
blockModel.props.count // 2
// subscribe the signal when value change
blockModel.props.count$.subscribe(value => {
console.log('count is: ', value);
})
// update with yjs
blockModel.yBlock.set('prop:count', 3)
blockModel.props.count // 3
blockModel.props.count$ // { value: 3 }
Y.Map and Native Object
By default, all the props in a block model are stored in a Y.Map:
// in block model
{
foo: number,
bar: string
}
// in yjs
{
'prop:foo': number,
'prop:bar': string
}
Y.Array and Native Array
⚠️ Using array in block props is not recommended because Y.Array is a linked list so it's very different with native javascript array.
if a prop is defined as array, it will use Y.Array under the hood:
// in block model
{
options: string[]
}
// in yjs
{
'prop:options': Y.Array<string>
}
Y.Text and Text
We provide you an Text
data structure which use Y.Text under the hood to make it easier to manipulate the text data:
import { Text } from '@blocksuite/store'
// in block model
{
text: Text
}
// in yjs
{
'prop:text': Y.Text
}
To define a property with Text
type in block schema, you can use internal.Text
:
defineBlockSchema({
// ...
props: (internal): ParagraphProps => ({
text: internal.Text(),
}),
})
The Text
data type can be used like:
blockModel.props.text.insert('Hello World', 0);
// [{ insert: "Hello World" }]
blockModel.props.text.delete(5, 6);
// [{ insert: "Hello" }]
blockModel.props.text.join(new Text(' BlockSuite'));
// [{ insert: "Hello BlockSuite" }]
blockModel.props.text.format(6, 10, { bold: true });
// [{ insert: "Hello " }, { insert: "BlockSuite", attributes: { bold: true } }]
Boxed Data Type
What if you want to declare a piece of data that won't be transformed to yjs? You can use internal.Boxed
to do that:
import { Boxed } from '@blocksuite/store'
// in block model
{
data: Boxed<Array<string>>
}
// in yjs
{
'prop:data': Y.Map<{ value: Array<string> }>
}
It's a little bit tricky, but you can also use Yjs data in boxed data to avoid it to be converted into native yjs.
defineBlockSchema({
// ...
props: (internal): ParagraphProps => ({
someData: internal.Boxed(new Y.Array<string>()),
}),
})
model.someData.value; // Y.Array
model.someData.value.insert(0, ["foo", "bar"]); // Y.Array<["foo", "bar"]>
Nested Objects and Flat Data
In blocksuite, there are two modes for handling nested object structures: default mode and flat mode. Below, I will introduce each mode and explain their differences.
Default Mode
In default mode, a nested Y.Map
structure is used to store the data. For example:
// Block model
{
propertyA: {
foo: 0,
bar: {
message: 'ok'
}
}
}
// In yjs
Y.Map<{
'prop:propertyA': Y.Map<{
foo: 0,
bar: Y.Map<{
message: 'ok'
}>
}>
}>
However, this approach is not recommended if your data contains many dynamically nested objects, as it can easily lead to data loss. In such cases, we suggest using flat mode instead.
Flat Mode
In flat mode, all data is flattened and stored in a single Y.Map
. For example:
// Block model
{
propertyA: {
foo: 0,
bar: {
message: 'ok'
}
}
}
// In yjs
Y.Map<{
'prop:propertyA.foo': 0,
'prop:propertyA.bar.message': 'ok'
}>
With this structure, changes to dynamic nested objects will not result in data loss. Flat mode is therefore recommended when working with data that includes many dynamic nested objects.