Skip to Content
For DevelopersMigrations

Migrations

When you change a block’s props shape in a backwards-incompatible way — renaming a field, merging two fields, restructuring nested data — existing saved documents still reference the old shape. Migrations let you upgrade old blocks on-the-fly when they load.

The idea

  1. Each component has a version (default 0).
  2. Each saved block instance records its version.
  3. When the editor loads a document, the migration runner walks each block from its recorded version up to the component’s current version, running each migration in order.

Migrations only run in memory. The original saved JSON is untouched until the user saves again.

Declaring a migration

import { defineMigration, type ComponentConfig } from "@useblok/core"; // v0 → v1: rename `heading` to `title`. const migrateV0ToV1 = defineMigration< { heading: string }, { title: string } >({ from: 0, to: 1, migrate: (props) => ({ title: props.heading, }), }); const Hero: ComponentConfig<{ title: string }> = { label: "Hero", version: 1, migrations: [migrateV0ToV1], fields: { title: { type: "text", label: "Title" } }, defaultProps: { title: "Hello" }, render: ({ title }) => <h1>{title}</h1>, };

Multi-step migrations

Chain migrations for each step of your schema evolution:

version: 3, migrations: [ migrateV0ToV1, // rename heading → title migrateV1ToV2, // split title into title + subtitle migrateV2ToV3, // move subtitle into a `hero.meta` object ],

The runner picks the right starting point per-block. If a block is already at v2, only migrateV2ToV3 runs on it.

migrate function signature

migrate: (oldProps) => newProps

It’s a pure function. Don’t trigger side effects — it may run many times per session (once per block).

When props depend on other data

Sometimes you need to look at the full document to migrate a block (e.g. “if the root has darkMode: true, set this block’s theme to dark”).

The runner doesn’t pass the full doc, but you can migrate the document yourself before passing it to <Blok>:

import { migrateData } from "@useblok/core"; const upgraded = migrateData(data, config); // Now do whatever document-level fix-ups you need upgraded.content = upgraded.content.map((block) => { if (block.type === "Hero" && data.root.props?.darkMode) { return { ...block, props: { ...block.props, theme: "dark" } }; } return block; }); <Blok config={config} data={upgraded} />

Migration report

migrateData returns the upgraded document and a report:

const { data: upgraded, report } = migrateData(originalData, config); report.migratedBlocks.forEach((entry) => { console.log(`${entry.blockId}: v${entry.fromVersion} → v${entry.toVersion}`); });

Inspect the report in dev to catch missed migrations before deploying.

Removing a block type

If you completely remove a block type, existing saved documents will have references to the missing type. Options:

  1. Leave a stub — keep a minimal ComponentConfig that just renders a placeholder. Users can then delete the blocks themselves.
  2. Filter at load — strip removed types from data.content / data.zones before passing to Blok.

Don’t silently drop unknown block types without telling anyone — users may lose content without realising. Log it, warn, or show an “unknown block” placeholder.

Versioning field types (plugins)

Custom field types from plugins don’t have their own migration system — if the plugin changes its field shape, you need to migrate the block that uses it.

Last updated on