本文へスキップ
ブログに戻る

Turning Editor.js JSON into React components

約7分2回閲覧

Why store JSON instead of HTML#

When picking an editor for this site, the thing that settled it for Editor.js was that it does not hand back one lump of HTML — it hands back a list of blocks as JSON. That sounds like a small difference. It changes everything downstream.

  • Content stays data rather than markup, so building a table of contents from the header blocks or computing reading time from the prose is direct work.
  • The presentation can change without touching a single stored row, because the styling lives in components rather than inside the content.
  • The surface where raw HTML has to be trusted shrinks to inline formatting instead of a whole article.
  • The same content can be reused for RSS, meta descriptions and search indexing without stripping tags first.

The shape you actually get#

json
{
  "time": 1754006400000,
  "blocks": [
    { "id": "a1", "type": "header", "data": { "text": "Overview", "level": 2 } },
    { "id": "a2", "type": "paragraph", "data": { "text": "Text can be <b>bold</b>." } }
  ],
  "version": "2.31.0"
}

Mapping block types to components#

The whole renderer hinges on one lookup table from block.type to a component. Enabling a new tool in the editor means adding one line here, and nowhere else.

javascript
const RENDERERS: Record<string, ComponentType<BlockProps>> = {
  header: HeaderBlock,
  paragraph: ParagraphBlock,
  list: ListBlock,
  checklist: ChecklistBlock,
  quote: QuoteBlock,
  code: CodeBlock,
  table: TableBlock,
  delimiter: DelimiterBlock,
}

export function Blocks({ doc }: { doc: EditorDocument }) {
  return doc.blocks.map((block) => {
    const Block = RENDERERS[block.type]
    if (!Block) return null // unsupported tool: skip it, never crash the page
    return <Block key={block.id} data={block.data} />
  })
}
An unrecognised block should disappear quietly. It should never take the page down with it.
First rule of rendering user-authored content

The parts that catch people out#

  1. Block text is always inline HTML (<b>, <i>, links). Strip the tags before reusing it in a table of contents or an excerpt.
  2. Never assume data has the shape you expect — it comes out of a JSON column, so narrow it before you read it.
  3. @editorjs/list v2 changed items from an array of strings to an array of objects carrying content, meta and nested items.
  4. The checklist block keys its items on text, not on content the way list does. This one bites almost everybody once.
  • Write a component for every block type enabled in the editor
  • Fail soft on unknown blocks and missing fields
  • Strip inline tags before using text in the TOC and meta tags
  • Add image and embed blocks once there are real media files
BlockKey fieldsWatch out for
headertext, levellevel can be 1 to 6; clamp it to h2/h3 before building a TOC
liststyle, items[]v2 items are objects, no longer plain strings
checklistitems[].text, items[].checkedUses text, unlike list which uses content
tablewithHeadings, content[][]The first row is a header row only when withHeadings is true