DEV Community

dragogargo
dragogargo

Posted on

How to Migrate from Draft.js to Lexical in 2026 (Complete Guide)

Draft.js is archived. No maintenance, no security patches, 820K weekly downloads running on momentum. Meta says use Lexical, but never shipped a migration path — issues #1641 and #2197 have been open since 2022.

I built draft-to-lexical because I needed it and it didn't exist.

The problem
Draft.js content (RawDraftContentState) is a flat list of blocks with style ranges and an entity map. Lexical (SerializedEditorState) is a nested node tree. You can't rename a few fields and call it done — the structures are incompatible.

Draft.js:

{
"blocks": [
{
"text": "Hello bold world",
"type": "unstyled",
"inlineStyleRanges": [
{ "offset": 6, "length": 4, "style": "BOLD" }
]
}
],
"entityMap": {}
}
Lexical:

{
"root": {
"type": "root",
"children": [
{
"type": "paragraph",
"children": [
{ "type": "text", "text": "Hello ", "format": 0 },
{ "type": "text", "text": "bold", "format": 1 },
{ "type": "text", "text": " world", "format": 0 }
]
}
]
}
}
The differences that matter:

Flat vs. nested: Draft.js blocks are a flat array. Lexical nodes form a tree.
Style ranges vs. format flags: Draft.js uses offset/length ranges. Lexical uses bitwise flags on each text node.
Entity map vs. wrapper nodes: Draft.js stores links in a separate entityMap. Lexical wraps text inside LinkNode.
List items: Draft.js keeps them as flat blocks with a depth field. Lexical nests listitem nodes inside list nodes.
Install

npm install draft-to-lexical
As a library

import { convert } from 'draft-to-lexical';

const draftContent = {
blocks: [
{
key: 'a1',
text: 'Hello bold world',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [{ offset: 6, length: 4, style: 'BOLD' }],
entityRanges: [],
data: {},
},
],
entityMap: {},
};

const lexicalState = convert(draftContent);
As a CLI

Single file

draft-to-lexical convert input.json -o output.json --pretty

Pipe from stdin

cat draft-content.json | draft-to-lexical convert --pretty

Batch convert a directory

draft-to-lexical batch ./draft-files/ -o ./lexical-files/ --pretty
What gets converted
Block types
Draft.js Lexical
unstyled paragraph
header-one ... header-six heading (h1-h6)
blockquote quote
code-block code
unordered-list-item listitem inside list (bullet)
ordered-list-item listitem inside list (number)
atomic Depends on entity
Inline styles
All standard styles map to Lexical's bitwise format flags: BOLD (1), ITALIC (2), UNDERLINE (4), STRIKETHROUGH (8), CODE (16), HIGHLIGHT (32), SUBSCRIPT (64), SUPERSCRIPT (128).

Overlapping styles get OR'd together — bold + italic = format flag 3.

Entities
Links become Lexical LinkNode wrapping text children. Images become ImageNode with src, alt, width, height.

Custom entities
Every Draft.js project has custom entities — mentions, embeds, file attachments. The converter lets you write handlers for those:

import { convert } from 'draft-to-lexical';

const result = convert(draftContent, {
entityHandlers: {
MENTION: (entity, text, children) => ({
type: 'mention',
version: 1,
mentionName: entity.data.name,
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
}),
VIDEO: (entity) => ({
type: 'video',
version: 1,
src: entity.data.src,
}),
},
});
Migrating a database
If you have thousands of documents stored as Draft.js JSON, use the batch CLI or write a migration script:

import { convert } from 'draft-to-lexical';
import { db } from './your-database';

const documents = await db.query('SELECT id, content FROM documents');

for (const doc of documents) {
const draftContent = JSON.parse(doc.content);
const lexicalState = convert(draftContent);
await db.query(
'UPDATE documents SET content = $1 WHERE id = $2',
[JSON.stringify(lexicalState), doc.id]
);
}

Links

https://github.com/dragogargo/draft-to-lexical
https://www.npmjs.com/package/draft-to-lexical

Top comments (0)