useBlocks(): работа с блоками из React
Реактивный снимок дерева блоков и полный API для изменений, от адаптеров фреймворков. Используйте хук useBlocks(editor, options?) в @bloklabs/react, composable useBlocks(editor, options?) в @bloklabs/vue или injectBlocks(editor, options?) в @bloklabs/angular. В Angular передайте сигнал instance у BlokEditorComponent/BlokContentDirective и вызывайте его из контекста внедрения (инициализатора поля или конструктора).
Чтения реактивно перерисовываются при изменении документа. Записи атомарны: это один шаг отмены. Их также безопасно вызывать до готовности редактора, тогда они ничего не делают.
Возвращаемые объекты BlockNode ({ id, type, parentId, contentIds }) берутся из свежего снимка и быстро устаревают. Читайте их сразу и не сохраняйте в массивах зависимостей.
По умолчанию реактивность охватывает весь документ. Передайте { within: blockId }, чтобы перерисовка шла только при изменениях внутри поддерева этого блока: самого блока или любого его потомка.
Это нужно контейнерному блоку, который отрисовывает только собственных детей. Без ограничения области такой блок перерисовывается на каждое нажатие клавиши в любом месте документа, и страница из N контейнеров превращает одно нажатие в N перерисовок.
Область ограничивает перерисовки, а не чтения: с заданной областью вы по-прежнему видите всё дерево, поэтому getById/getChildren работают с любым блоком. Во Vue область принимает также ref или getter, а в Angular signal. Значение читается в момент отправки изменения, поэтому его смена не требует переподписки.
Методы
getById(id)
BlockNode | nullБлок с данным id как снимок-узел, или null для неизвестного id.
const node = blocks.getById('x9k2f1');
// → { id: 'x9k2f1', type: 'paragraph', parentId: null, contentIds: [] }getChildren(parentId)
BlockNode[]Прямые дети родительского блока в порядке документа. Передайте null для корневых блоков.
const rootBlocks = blocks.getChildren(null);
const rowBlocks = blocks.getChildren(databaseBlockId);insert(spec?)
BlockNode | nullВставить один блок (type, data, parentId, position, tunes, id, focus/caret, replace). replace: true вместе с position, который указывает на существующий блок, заменяет этот блок, а не вставляет рядом. Это программное «turn into».
Возвращает созданный узел или null при отказе:
- неизвестный тип инструмента
- несуществующий parentId
replaceс отсутствующей целью
Явно заданный id, который уже есть, работает как insert-if-absent. Вызов атомарный: один шаг отмены.
const node = blocks.insert({
type: 'header',
data: { text: 'New section', level: 2 },
position: 'end',
focus: true,
});
// → node.id is the new block's id (or null if rejected)insertMany(specs)
BlockNode[]Вставить несколько блоков атомарно, в порядке массива, ОДНИМ шагом отмены. Неудачные спецификации отбрасываются. Возвращаются созданные узлы.
const nodes = blocks.insertMany([
{ type: 'header', data: { text: 'Title' } },
{ type: 'paragraph', data: { text: 'Body' } },
]);insertTree(spec)
BlockNode | nullВставить готовое ВЛОЖЕННОЕ поддерево одной атомарной операцией. Дети вставляются под свой узел рекурсивно. Параметры размещения применяются только к корню. Возвращает корневой узел или null при отклонённом или конфликтующем id.
const root = blocks.insertTree({
type: 'toggle',
data: { text: 'Details' },
children: [
{ type: 'paragraph', data: { text: 'Hidden content' } },
],
});insertMarkdown(markdown, options?)
Promise<BlockNode[]>Преобразовать строку Markdown в блоки и вставить их АДДИТИВНО, без очистки документа. Вызов асинхронный, потому что конвертер загружается лениво. Возвращает все созданные узлы в порядке документа. Пустой ввод или несуществующий parentId возвращает [].
const nodes = await blocks.insertMarkdown(
'# Title\n\n- one\n- two',
{ position: 'end' },
);exportMarkdown()
Promise<string>Сериализовать ВЕСЬ документ в Markdown. Вызов асинхронный, сериализатор загружается лениво. Структура, которую Markdown не выражает, например объединённые ячейки таблицы, отбрасывается.
const md = await blocks.exportMarkdown();markdownToBlocks(md, config?)
Promise<OutputBlockData[]>Преобразовать Markdown в блоки БЕЗ экземпляра редактора. Это отдельный подпуть @bloklabs/core/markdown, а не метод хука.
Ему не нужны ни DOM, ни смонтированный Blok, поэтому он закрывает серверный сценарий, недоступный для insertMarkdown/exportMarkdown:
- импорт Markdown в задаче Node
- наполнение документа
- предвычисление
dataдо монтирования редактора
config это MarkdownImportConfig (сопоставление инструментов, GFM, расширения). Результат готов для blocks.render() или blocks.insertMany().
import { markdownToBlocks } from '@bloklabs/core/markdown';
// No editor instance required — this also runs on the server
const parsed = await markdownToBlocks('# Title\n\n- one\n- two');
// -> OutputBlockData[]; store it, or hand it to a live editor
await blocks.render({ blocks: parsed });markdownToBlocksWithReport(md, config?)
Promise<{ blocks: OutputBlockData[]; warnings: MarkdownDegradation[] }>Те же блоки плюс то, что Markdown не смог в них перенести. В Blok нет блока для сырого HTML, поэтому разметка, записанная в Markdown, экранируется и сохраняется как обычный текст. Исключение одно: голый <br> становится переносом строки. Экранирование безопасно, но происходит молча.
Берите этот метод, когда импорт идёт без присмотра (инструмент MCP, агент, массовая миграция) и кому-то нужно сообщить, что изменилось. Его исходящий близнец, blocksToMarkdownWithReport, лежит в @bloklabs/core/view.
import { markdownToBlocksWithReport } from '@bloklabs/core/markdown';
const { blocks: parsed, warnings } = await markdownToBlocksWithReport(source);
// warnings: [{ construct: 'html', action: 'degraded', detail: 'HTML is escaped and stored as literal text…' }]
if (warnings.length === 0) {
await blocks.render({ blocks: parsed });
}move(id, target)
voidПереместить блок в плоский слот: { before }, { after } или { toIndex }. Блок принимает родителя того места, куда попал. Чтобы сменить родителя без выбора соседнего слота, используйте nest/unnest.
blocks.move(nodeId, { after: otherId });
blocks.move(nodeId, { toIndex: 0 });nest(id, parentId)
voidСделать блок ребёнком другого блока.
blocks.nest(childId, toggleId);unnest(id)
voidПоднять вложенный блок на уровень выше (из его родителя).
blocks.unnest(childId);remove(id)
voidУдалить блок (и его поддерево).
blocks.remove(nodeId);update(id, data?, tunes?)
voidОбновить данные и/или тюны блока по id. Метод делегирует асинхронному blocks.update ядра, и это отдельный шаг отмены. Неизвестные id тихо превращаются в no-op.
blocks.update(nodeId, { text: 'Edited' });convert(id, newType, dataOverrides?, options?)
voidПреобразовать блок в другой тип («turn into»). Оба инструмента должны определять conversionConfig. Непреобразуемый блок корректно превращается в no-op. options.caret ставит курсор в преобразованный блок.
blocks.convert(nodeId, 'header', { level: 2 });transact(fn)
voidВыполнить несколько мутаций как ОДИН атомарный шаг отмены.
blocks.transact(() => {
blocks.remove(oldId);
blocks.insert({ type: 'paragraph', data: { text: 'Replacement' } });
});transactWithoutCapture(fn, options?)
voidКак transact, но операция НЕ попадает в историю отмены. Используйте его для тихой авто-починки или нормализации, через которую CMD+Z не должен проходить.
Передайте { derivedFrom: blockId, from: ['url'] }, если операции записывают данные, вычисленные из этих ключей блока, как в blocks.transactWithoutCapture ядра. Они входят в шаг отмены, который записал эти значения.
blocks.transactWithoutCapture(() => {
blocks.update(nodeId, { text: normalized });
});splitBlock(currentBlockId, currentBlockData, newBlockType, newBlockData, insertIndex)
BlockNode | nullАтомарно разделить блок: обновить текущий блок и вставить новый по абсолютному плоскому индексу, ОДНИМ шагом отмены.
const newNode = blocks.splitBlock(
nodeId, { text: 'First half' },
'paragraph', { text: 'Second half' },
blocks.getBlockIndex(nodeId)! + 1,
);insertInsideParent(parentId, insertIndex, childData?)
BlockNode | nullВставить одного ребёнка под родителя по плоскому индексу атомарно: создание И назначение родителя ОДНИМ шагом отмены. Это лучше, чем insert() + nest(), которые дают два шага.
Индекс ограничивается поддеревом родителя. Дочерний блок никогда не встаёт раньше родителя или за концом его поддерева.
const child = blocks.insertInsideParent(toggleId, 3);insertOutputData(blocks, options?)
BlockNode[]Вставить плоский массив уже сериализованных OutputBlockData (форма save()) напрямую, с учётом связей parent/content. Один атомарный шаг отмены.
const nodes = blocks.insertOutputData(savedFragment.blocks);render(data)
Promise<void>Заменить ВЕСЬ документ блоками из сохранённых OutputData. Это примитив ЗАГРУЗКИ документа: он сначала очищает содержимое, в отличие от аддитивных вставок.
await blocks.render(savedData);renderFromHTML(html)
Promise<void>Заменить ВЕСЬ документ блоками, разобранными из HTML-строки (сначала очищает содержимое).
await blocks.renderFromHTML('<h1>Imported</h1><p>Body</p>');clear()
Promise<void>Удалить все блоки из документа.
await blocks.clear();getBlocksCount()
numberТекущее количество блоков (реактивно).
const count = blocks.getBlocksCount();getCurrentBlockIndex()
numberПлоский индекс блока с курсором, или -1, если курсора нет.
const index = blocks.getCurrentBlockIndex();getBlockByIndex(index)
BlockNode | nullБлок по плоскому индексу как снимок-узел, или null.
const first = blocks.getBlockByIndex(0);getBlockIndex(id)
number | nullАбсолютный плоский индекс блока по id, или null для неизвестного.
const index = blocks.getBlockIndex(nodeId);getBlockData(id)
{ data, tunes } | nullПрочитать текущие данные и тюны блока по id, ничего не меняя. Так дубликат блока собирается прямо на клиенте: прочитать узел, затем insert({ type, data, tunes }).
Поля с форматированным текстом в data хранят фрагменты. data может создаваться заново при каждом вызове, поэтому не используйте его как зависимость мемоизации.
const saved = blocks.getBlockData(nodeId);
if (saved) {
blocks.insert({ type: 'paragraph', data: saved.data, position: { after: nodeId } });
}getBlockByElement(element)
BlockNode | nullБлок, чей holder содержит DOM-элемент или равен ему. Так цель события сопоставляется обратно с блоком.
const node = blocks.getBlockByElement(event.target as HTMLElement);composeBlockData(toolName)
Promise<BlockToolData>Прочитать пустые данные инструмента по умолчанию без вставки. Отклоняется для неизвестного инструмента.
const defaults = await blocks.composeBlockData('header');isSyncingFromYjs()
booleanИдёт ли сейчас синхронизация Yjs (undo/redo). Используйте это, чтобы пропустить очистку, которая конфликтовала бы с состоянием отмены.
if (!blocks.isSyncingFromYjs()) {
blocks.update(nodeId, { text: cleaned });
}import { useBlok, BlokContent, useBlocks } from '@bloklabs/react';
export function Outline() {
const editor = useBlok({ tools });
const blocks = useBlocks(editor);
// Reactive: re-renders whenever the document changes.
const rootBlocks = blocks.getChildren(null);
return (
<>
<BlokContent editor={editor} />
<ol>{rootBlocks.map((b) => <li key={b.id}>{b.type}</li>)}</ol>
</>
);
}
// Inside a container block: re-render only for its OWN subtree, so typing in an
// unrelated block elsewhere in the document costs this component nothing.
function Steps({ block }) {
const blocks = useBlocks(useBlokInstance(), { within: block.id });
const steps = blocks.getChildren(block.id);
return <ol>{steps.map((s) => <li key={s.id}>{s.type}</li>)}</ol>;
}