Frameworks

React

npm i @matrajs/react

The engine comes with it · there is no second package, and nothing third-party behind it.

An editor

import { starterKit } from '@matrajs/core'
import { EditorContent, useEditor } from '@matrajs/react'

export function Editor() {
  const editor = useEditor({
    extensions: starterKit,
    content: '<p>Hello.</p>',
  })

  return <EditorContent editor={editor} />
}

The editor is created once and destroyed on unmount. Options are read once · changing them later does not recreate it, because tearing down a live document on a prop change loses whatever the person was writing. Use commands instead.

A toolbar that stays honest

Commands mutate the document; React does not hear about that on its own, so a naive toolbar's active states go stale the moment the caret moves. useEditorState subscribes to both changes and selection changes.

import { useEditorState } from '@matrajs/react'

function BoldButton({ editor }) {
  const active = useEditorState(editor, (e) => e.isActive('bold'))
  return (
    <button
      aria-pressed={active}
      onMouseDown={(event) => {
        event.preventDefault()        // keep the caret in the editor
        editor.commands.toggleBold()
      }}
    >
      Bold
    </button>
  )
}
onMouseDown with preventDefault, not onClick. A click moves focus to the button first, which collapses the selection · so the command runs against a caret rather than the words you highlighted.

Buttons that know when they cannot

can is the same command asking rather than doing. A button reads it to be disabled instead of looking enabled and doing nothing when pressed — the caret is in a code block, or the selection cannot hold that mark, and the answer is available before the user finds out by pressing.

const canBold = useEditorState(editor, (e) => e.can.toggleBold())

<button disabled={!canBold} …>Bold</button>

Several changes, one undo

editor.batch((c) => {
  c.toggleBold()
  c.toggleItalic()
})

One history step, and it rolls back entirely if any command in it returns false.

Saving

useEffect(() => editor.on('change', () => save(editor.getJSON())), [editor])

on returns its own unsubscribe function, so it works as an effect cleanup directly.

Focus

const focused = useEditorFocus(editor)

True while the editor has DOM focus · useful for showing a toolbar only while the caret is actually in the document.

StrictMode double-invokes effects in development. The mount is guarded, so that cannot leave two views attached to one element — you do not need to disable StrictMode to use this.

Edit this page on GitHub