---
title: "useBlocks Hook — Blok React API"
description: "Read and mutate blocks from React with useBlocks, including insert, move, and subtree relocation."
source: https://blokeditor.com/docs/use-blocks/
lastmod: 2026-08-02
---

Framework JavaScript

Framework adapters useBlocks

On this page getById(id)

# useBlocks(): read and mutate blocks from React

A reactive snapshot of the block tree plus a full manipulation API, from the framework adapters: the useBlocks(editor) hook in @bloklabs/react, the useBlocks(editor) composable in @bloklabs/vue, and `injectBlocks(editor)` in @bloklabs/angular — pass the `instance` signal of BlokEditorComponent/BlokContentDirective, and call it from an injection context (a field initializer or the constructor). Reads re-render reactively as the document changes; writers are atomic (one undo step) and safe to call before the editor is ready (they no-op). Returned BlockNode objects ({ id, type, parentId, contentIds }) are fresh-snapshot volatile — read them now, don't stash them in dep arrays.

Last updated Jul 17, 2026 [Edit this page on GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/api-data.ts)

## Methods

### getById(id)

BlockNode | null

The block with the given id as a snapshot node, or null when unknown.

TypeScript

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

### getChildren(parentId)

BlockNode[]

The direct children of a parent block, in document order. Pass null for the root blocks.

TypeScript

```
const rootBlocks = blocks.getChildren(null);
const rowBlocks = blocks.getChildren(databaseBlockId);
```

### insert(spec?)

BlockNode | null

Insert one block (type, data, parentId, position, tunes, id, focus/caret, replace). `replace: true` combined with a `position` that targets an existing block replaces that block instead of inserting beside it — a programmatic "turn into". Returns the created node, or null when rejected (unknown tool type, dangling parentId, or a `replace` whose target is missing). An explicit id that already exists is insert-if-absent. Atomic — one undo step.

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

Insert several blocks atomically, in array order, as ONE undo step. Specs that fail are dropped; returns the successfully created nodes.

TypeScript

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

### insertTree(spec)

BlockNode | null

Insert a pre-built NESTED subtree in one atomic operation. Children are inserted under their enclosing node recursively; placement options apply to the root only. Returns the root node, or null on a rejected/colliding id.

TypeScript

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

### insertMarkdown(markdown, options?)

Promise<BlockNode[]>

Convert a Markdown string to blocks and insert them ADDITIVELY (without clearing the document). Async — the converter is lazy-loaded. Returns all created nodes in document order; empty input or a dangling parentId returns [].

TypeScript

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

### exportMarkdown()

Promise<string>

Serialize the WHOLE document to Markdown (async, lazy-loaded serializer). Structure Markdown can't express (e.g. merged table cells) is dropped.

TypeScript

```
const md = await blocks.exportMarkdown();
```

### markdownToBlocks(md, config?)

Promise<OutputBlockData[]>

Convert Markdown to blocks WITHOUT an editor instance — the standalone `@bloklabs/core/markdown` subpath, not a method on the hook. It needs no DOM and no mounted Blok, so it is the server-side path insertMarkdown/exportMarkdown cannot cover: import Markdown in a Node job, seed a document, or precompute `data` before the editor mounts. `config` is a `MarkdownImportConfig` (tool mapping, GFM, extensions). The result is ready for `blocks.render()` or `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 });
```

### move(id, target)

void

Move a block to a flat slot: { before }, { after }, or { toIndex }. The block adopts the parent of wherever it lands — use nest/unnest to change the parent without picking a sibling slot.

TypeScript

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

### nest(id, parentId)

void

Make a block a child of another block.

TypeScript

```
blocks.nest(childId, toggleId);
```

### unnest(id)

void

Move a nested block up one level (out of its parent).

TypeScript

```
blocks.unnest(childId);
```

### remove(id)

void

Remove a block (and its subtree).

TypeScript

```
blocks.remove(nodeId);
```

### update(id, data?, tunes?)

void

Update a block's data and/or tunes by id. Delegates to core's async blocks.update (its own undo step); unknown ids are a silent no-op.

TypeScript

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

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

void

Convert a block to another type ("turn into"). Both tools must define conversionConfig; a non-convertible block is a graceful no-op. options.caret places the caret in the converted block.

TypeScript

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

### transact(fn)

void

Run several mutations as ONE atomic undo step.

TypeScript

```
blocks.transact(() => {
  blocks.remove(oldId);
  blocks.insert({ type: 'paragraph', data: { text: 'Replacement' } });
});
```

### transactWithoutCapture(fn)

void

Like transact, but the operation is NOT captured in undo history — for silent auto-repair/normalization that CMD+Z should never step through.

TypeScript

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

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

BlockNode | null

Atomically split a block: update the current block and insert a new one at an absolute flat index, as ONE undo step.

TypeScript

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

### insertInsideParent(parentId, insertIndex, childData?)

BlockNode | null

Insert a single child under a parent at a flat index, atomically (creation AND parent assignment in ONE undo step) — prefer over insert() + nest(), which is two steps.

TypeScript

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

### insertOutputData(blocks, options?)

BlockNode[]

Insert a flat array of already-serialized OutputBlockData (the save() shape) directly, honoring parent/content links. One atomic undo step.

TypeScript

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

### render(data)

Promise<void>

Replace the WHOLE document with blocks from saved OutputData — a document-LOAD primitive that clears existing content first (unlike the additive inserters).

TypeScript

```
await blocks.render(savedData);
```

### renderFromHTML(html)

Promise<void>

Replace the WHOLE document with blocks parsed from an HTML string (clears existing content first).

TypeScript

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

### clear()

Promise<void>

Remove every block from the document.

TypeScript

```
await blocks.clear();
```

### getBlocksCount()

number

The current block count (reactive).

TypeScript

```
const count = blocks.getBlocksCount();
```

### getCurrentBlockIndex()

number

The flat index of the block holding the caret, or -1 when none.

TypeScript

```
const index = blocks.getCurrentBlockIndex();
```

### getBlockByIndex(index)

BlockNode | null

The block at a flat index as a snapshot node, or null.

TypeScript

```
const first = blocks.getBlockByIndex(0);
```

### getBlockIndex(id)

number | null

The absolute flat index of a block by id, or null when unknown.

TypeScript

```
const index = blocks.getBlockIndex(nodeId);
```

### getBlockData(id)

{ data, tunes } | null

Read a block's current data and tunes by id without mutating anything — makes a client-side duplicate composable: read a node, then insert({ type, data, tunes }).

TypeScript

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

### getBlockByElement(element)

BlockNode | null

The block whose holder contains/equals a DOM element — maps an event target back to a block.

TypeScript

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

### composeBlockData(toolName)

Promise<BlockToolData>

Read a tool's default empty data without inserting anything. Rejects for an unknown tool.

TypeScript

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

### isSyncingFromYjs()

boolean

Whether a Yjs sync (undo/redo) is in progress — use it to skip cleanup that would fight undo state.

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>
    </>
  );
}
```
