Перейти к содержимому
ФреймворкJavaScript

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. Значение читается в момент отправки изменения, поэтому его смена не требует переподписки.

Обновлено 7 окт. 2026 г.Редактировать на GitHub

Методы

getById(id)

BlockNode | null

Блок с данным id как снимок-узел, или null для неизвестного id.

TypeScript
const node = blocks.getById('x9k2f1');
// → { id: 'x9k2f1', type: 'paragraph', parentId: null, contentIds: [] }

getChildren(parentId)

BlockNode[]

Прямые дети родительского блока в порядке документа. Передайте null для корневых блоков.

TypeScript
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. Вызов атомарный: один шаг отмены.

TypeScript
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[]

Вставить несколько блоков атомарно, в порядке массива, ОДНИМ шагом отмены. Неудачные спецификации отбрасываются. Возвращаются созданные узлы.

TypeScript
const nodes = blocks.insertMany([
  { type: 'header', data: { text: 'Title' } },
  { type: 'paragraph', data: { text: 'Body' } },
]);

insertTree(spec)

BlockNode | null

Вставить готовое ВЛОЖЕННОЕ поддерево одной атомарной операцией. Дети вставляются под свой узел рекурсивно. Параметры размещения применяются только к корню. Возвращает корневой узел или null при отклонённом или конфликтующем id.

TypeScript
const root = blocks.insertTree({
  type: 'toggle',
  data: { text: 'Details' },
  children: [
    { type: 'paragraph', data: { text: 'Hidden content' } },
  ],
});

insertMarkdown(markdown, options?)

Promise<BlockNode[]>

Преобразовать строку Markdown в блоки и вставить их АДДИТИВНО, без очистки документа. Вызов асинхронный, потому что конвертер загружается лениво. Возвращает все созданные узлы в порядке документа. Пустой ввод или несуществующий parentId возвращает [].

TypeScript
const nodes = await blocks.insertMarkdown(
  '# Title\n\n- one\n- two',
  { position: 'end' },
);

exportMarkdown()

Promise<string>

Сериализовать ВЕСЬ документ в Markdown. Вызов асинхронный, сериализатор загружается лениво. Структура, которую Markdown не выражает, например объединённые ячейки таблицы, отбрасывается.

TypeScript
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().

TypeScript
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.

TypeScript
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.

TypeScript
blocks.move(nodeId, { after: otherId });
blocks.move(nodeId, { toIndex: 0 });

nest(id, parentId)

void

Сделать блок ребёнком другого блока.

TypeScript
blocks.nest(childId, toggleId);

unnest(id)

void

Поднять вложенный блок на уровень выше (из его родителя).

TypeScript
blocks.unnest(childId);

remove(id)

void

Удалить блок (и его поддерево).

TypeScript
blocks.remove(nodeId);

update(id, data?, tunes?)

void

Обновить данные и/или тюны блока по id. Метод делегирует асинхронному blocks.update ядра, и это отдельный шаг отмены. Неизвестные id тихо превращаются в no-op.

TypeScript
blocks.update(nodeId, { text: 'Edited' });

convert(id, newType, dataOverrides?, options?)

void

Преобразовать блок в другой тип («turn into»). Оба инструмента должны определять conversionConfig. Непреобразуемый блок корректно превращается в no-op. options.caret ставит курсор в преобразованный блок.

TypeScript
blocks.convert(nodeId, 'header', { level: 2 });

transact(fn)

void

Выполнить несколько мутаций как ОДИН атомарный шаг отмены.

TypeScript
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 ядра. Они входят в шаг отмены, который записал эти значения.

TypeScript
blocks.transactWithoutCapture(() => {
  blocks.update(nodeId, { text: normalized });
});

splitBlock(currentBlockId, currentBlockData, newBlockType, newBlockData, insertIndex)

BlockNode | null

Атомарно разделить блок: обновить текущий блок и вставить новый по абсолютному плоскому индексу, ОДНИМ шагом отмены.

TypeScript
const newNode = blocks.splitBlock(
  nodeId, { text: 'First half' },
  'paragraph', { text: 'Second half' },
  blocks.getBlockIndex(nodeId)! + 1,
);

insertInsideParent(parentId, insertIndex, childData?)

BlockNode | null

Вставить одного ребёнка под родителя по плоскому индексу атомарно: создание И назначение родителя ОДНИМ шагом отмены. Это лучше, чем insert() + nest(), которые дают два шага.

Индекс ограничивается поддеревом родителя. Дочерний блок никогда не встаёт раньше родителя или за концом его поддерева.

TypeScript
const child = blocks.insertInsideParent(toggleId, 3);

insertOutputData(blocks, options?)

BlockNode[]

Вставить плоский массив уже сериализованных OutputBlockData (форма save()) напрямую, с учётом связей parent/content. Один атомарный шаг отмены.

TypeScript
const nodes = blocks.insertOutputData(savedFragment.blocks);

render(data)

Promise<void>

Заменить ВЕСЬ документ блоками из сохранённых OutputData. Это примитив ЗАГРУЗКИ документа: он сначала очищает содержимое, в отличие от аддитивных вставок.

TypeScript
await blocks.render(savedData);

renderFromHTML(html)

Promise<void>

Заменить ВЕСЬ документ блоками, разобранными из HTML-строки (сначала очищает содержимое).

TypeScript
await blocks.renderFromHTML('<h1>Imported</h1><p>Body</p>');

clear()

Promise<void>

Удалить все блоки из документа.

TypeScript
await blocks.clear();

getBlocksCount()

number

Текущее количество блоков (реактивно).

TypeScript
const count = blocks.getBlocksCount();

getCurrentBlockIndex()

number

Плоский индекс блока с курсором, или -1, если курсора нет.

TypeScript
const index = blocks.getCurrentBlockIndex();

getBlockByIndex(index)

BlockNode | null

Блок по плоскому индексу как снимок-узел, или null.

TypeScript
const first = blocks.getBlockByIndex(0);

getBlockIndex(id)

number | null

Абсолютный плоский индекс блока по id, или null для неизвестного.

TypeScript
const index = blocks.getBlockIndex(nodeId);

getBlockData(id)

{ data, tunes } | null

Прочитать текущие данные и тюны блока по id, ничего не меняя. Так дубликат блока собирается прямо на клиенте: прочитать узел, затем insert({ type, data, tunes }).

Поля с форматированным текстом в data хранят фрагменты. data может создаваться заново при каждом вызове, поэтому не используйте его как зависимость мемоизации.

TypeScript
const saved = blocks.getBlockData(nodeId);
if (saved) {
  blocks.insert({ type: 'paragraph', data: saved.data, position: { after: nodeId } });
}

getBlockByElement(element)

BlockNode | null

Блок, чей holder содержит DOM-элемент или равен ему. Так цель события сопоставляется обратно с блоком.

TypeScript
const node = blocks.getBlockByElement(event.target as HTMLElement);

composeBlockData(toolName)

Promise<BlockToolData>

Прочитать пустые данные инструмента по умолчанию без вставки. Отклоняется для неизвестного инструмента.

TypeScript
const defaults = await blocks.composeBlockData('header');

isSyncingFromYjs()

boolean

Идёт ли сейчас синхронизация Yjs (undo/redo). Используйте это, чтобы пропустить очистку, которая конфликтовала бы с состоянием отмены.

TypeScript
if (!blocks.isSyncingFromYjs()) {
  blocks.update(nodeId, { text: cleaned });
}
TypeScript
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>;
}