React package for ProseMirror Adapter
npm install @prosemirror-adapter/reactReact adapter for ProseMirror.
You can view the example in prosemirror-adapter/examples/react.

``bash`
npm install @prosemirror-adapter/react
`tsx
import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/react'
import { YourAwesomeEditor } from 'somewhere'
export function Component() {
return (
)
}
`
In this section we will implement a node view for paragraph node.
#### Build component for node view
`tsx
import { useNodeViewContext } from '@prosemirror-adapter/react'
function Paragraph() {
const { contentRef, selected } = useNodeViewContext()
return
#### Bind node view components with prosemirror
`tsx
import { useNodeViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'import { Paragraph } from './Paragraph'
export const YourAwesomeEditor: FC = () => {
const nodeViewFactory = useNodeViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: YourProsemirrorEditorState,
nodeViews: {
paragraph: nodeViewFactory({
component: Paragraph,
// Optional: add some options
as: 'div',
contentAs: 'p',
}),
},
})
},
[nodeViewFactory],
)
return
}
`š Congratulations! You have built your first react node view with prosemirror-adapter.
$3
In this section we will implement a mark view for links that changes color periodically.
#### Build component for mark view
`tsx
import { useEffect, useState } from 'react'
import { useMarkViewContext } from '@prosemirror-adapter/react'const colors = [
'#f06292',
'#ba68c8',
'#9575cd',
'#7986cb',
'#64b5f6',
'#4fc3f7',
'#4dd0e1',
'#4db6ac',
'#81c784',
'#aed581',
'#ffb74d',
'#ffa726',
'#ff8a65',
'#d4e157',
'#ffd54f',
'#ffecb3',
]
function pickRandomColor() {
return colors[Math.floor(Math.random() * colors.length)]
}
export function Link() {
const [color, setColor] = useState(colors[0])
const { mark, contentRef } = useMarkViewContext()
const href = mark.attrs.href as string
const title = mark.attrs.title as string | null
useEffect(() => {
const interval = setInterval(() => {
setColor(pickRandomColor())
}, 1000)
return () => clearInterval(interval)
}, [])
return (
href={href}
ref={contentRef}
style={{ color, transition: 'color 1s ease-in-out' }}
title={title || undefined}
>
)
}
`#### Bind mark view components with prosemirror
`tsx
import { useMarkViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback } from 'react'export const YourAwesomeEditor: FC = () => {
const markViewFactory = useMarkViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
props: {
markViews: {
link: markViewFactory({
component: Link,
}),
},
},
}),
],
}),
})
},
[markViewFactory],
)
return
}
`š Congratulations! You have built your first react mark view with prosemirror-adapter.
$3
In this section we will implement a plugin view that will display the size of the document.
#### Build component for plugin view
`tsx
import { usePluginViewContext } from '@prosemirror-adapter/react'function Size() {
const { view } = usePluginViewContext()
const size = view.state.doc.nodeSize
return
Size for document: {size}
}
`#### Bind plugin view components with prosemirror
`tsx
import { usePluginViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'
import { Plugin } from 'prosemirror-state'import { Paragraph } from './Paragraph'
export const YourAwesomeEditor: FC = () => {
const pluginViewFactory = usePluginViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
view: pluginViewFactory({
component: Size,
}),
}),
],
}),
})
},
[pluginViewFactory],
)
return
}
`š Congratulations! You have built your first react plugin view with prosemirror-adapter.
$3
In this section we will implement a widget view that will add hashes for heading when selected.
#### Build component for widget decoration view
`tsx
import { useWidgetViewContext } from '@prosemirror-adapter/react'export function Hashes() {
const { spec } = useWidgetViewContext()
const level = spec?.level
const hashes = Array(level || 0)
.fill('#')
.join('')
return {hashes}
}
`#### Bind widget view components with prosemirror
`tsx
import { useWidgetViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'
import { Plugin } from 'prosemirror-state'import { Hashes } from './Hashes'
export const YourAwesomeEditor: FC = () => {
const widgetViewFactory = useWidgetViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const getHashWidget = widgetViewFactory({
as: 'i',
component: Hashes,
})
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
props: {
decorations(state) {
const { $from } = state.selection
const node = $from.node()
if (node.type.name !== 'heading') return DecorationSet.empty
const widget = getHashWidget($from.before() + 1, {
side: -1,
level: node.attrs.level,
})
return DecorationSet.create(state.doc, [widget])
},
},
}),
],
}),
})
},
[widgetViewFactory],
)
return
}
`š Congratulations! You have built your first react widget view with prosemirror-adapter.
API
$3
#### useNodeViewFactory: () => (options: NodeViewFactoryOptions) => NodeView
`ts
type DOMSpec = string | HTMLElement | ((node: Node) => HTMLElement)interface NodeViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the node view.
as?: DOMSpec
// The DOM element that contains the content of the node.
contentAs?: DOMSpec
// Overrides: this part is equal to properties of NodeView
update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean | void
ignoreMutation?: (mutation: ViewMutationRecord) => boolean | void
selectNode?: () => void
deselectNode?: () => void
setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void
stopEvent?: (event: Event) => boolean
destroy?: () => void
// Called when the node view is updated.
onUpdate?: () => void
}
`#### useNodeViewContext: () => NodeViewContext
`ts
interface NodeViewContext {
// The DOM element that contains the content of the node.
contentRef: NodeViewContentRef // The prosemirror editor view.
view: EditorView
// Get prosemirror position of current node view.
getPos: () => number | undefined
// Set node.attrs of current node.
setAttrs: (attrs: Attrs) => void
// The prosemirror node for current node.
node: Node
// The prosemirror decorations for current node.
decorations: readonly Decoration[]
// The prosemirror inner decorations for current node.
innerDecorations: DecorationSource
// Whether the node is selected.
selected: boolean
}
`
$3
#### useMarkViewFactory: () => (options: MarkViewFactoryOptions) => MarkView
`ts
type MarkViewDOMSpec = string | HTMLElement | ((mark: Mark) => HTMLElement)interface MarkViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the mark view
as?: MarkViewDOMSpec
// The DOM element that contains the content of the mark
contentAs?: MarkViewDOMSpec
// Called when the mark view is destroyed
destroy?: () => void
}
`#### useMarkViewContext: () => MarkViewContext
`ts
interface MarkViewContext {
// The DOM element that contains the content of the mark
contentRef: MarkViewContentRef // The prosemirror editor view
view: EditorView
// The prosemirror mark for current mark view
mark: Mark
// Whether the mark is inline
inline: boolean
}
`
$3
#### usePluginViewFactory: () => (options: PluginViewFactoryOptions) => PluginView
`ts
interface PluginViewFactoryOptions {
// Component
component: ReactComponent // The DOM element to use as the root node of the plugin view.
// The
viewDOM here means EditorState.view.dom.
// By default, it will be EditorState.view.dom.parentElement.
root?: (viewDOM: HTMLElement) => HTMLElement // Overrides: this part is equal to properties of PluginView
update?: (view: EditorView, prevState: EditorState) => void
destroy?: () => void
}
`#### usePluginViewContext: () => PluginViewContext
`ts
interface PluginViewContext {
// The prosemirror editor view.
view: EditorView // The previously prosemirror editor state.
// Will be
undefined when the plugin view is created.
prevState?: EditorState
}
`
$3
#### useWidgetViewFactory: () => (options: WidgetViewFactoryOptions) => WidgetDecorationFactory
`ts
type WidgetDecorationFactory = (pos: number, spec?: WidgetDecorationSpec) => Decorationinterface WidgetViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the widget view.
as: string | HTMLElement
}
`#### useWidgetViewContext: () => WidgetViewContext
`ts
interface WidgetViewContext {
// The prosemirror editor view.
view: EditorView // Get the position of the widget.
getPos: () => number | undefined
// Get the spec of the widget.
spec?: WidgetDecorationSpec
}
`Troubleshooting
I'm getting an error: "flushSync was called from inside a lifecycle method"
This can happen if you're adding or removing a plugin to the editor inside a lifecycle method (e.g.
useEffect and useLayoutEffect), like the code block below.`tsx
import { addPlugin, removePlugin, nodeViewPlugin } from './utils'function MyEditor() {
const [enablePlugin, setEnablePlugin] = useState(true)
useEffect(() => {
if (!enablePlugin) return
const view = viewRef.current
// Add or remove a new plugin to the editor, which renders node view using React components by
// using
prosemirror-adapter under the hood.
addPlugin(view, nodeViewPlugin)
return () => removePlugin(view, nodeViewPlugin)
}, [enablePlugin]) // ...
}
`When updating such a plugin, ProseMirror might need to redraw some nodes using (or not using) React components. During this process, ProseMirror will first stop the
DOMObserver, redraw the nodes, and then resume the DOMObserver. This process is synchronous, so React.flushSync is called internally to ensure the React components are updated before the DOMObserver resumes.This is roughly equivalent to:
`tsx
useEffect(() => {
React.flushSync(() => {
setSomething(newValue)
})
}, [])
`This pattern violates React's rules.
To fix this, put the plugin update logic inside a _task_ (via
setTimeout) or _microtask_ (via queueMicrotask).The example code above can be fixed by:
`tsx
import { addPlugin, removePlugin, nodeViewPlugin } from './utils'function MyEditor() {
const [enablePlugin, setEnablePlugin] = useState(true)
useEffect(() => {
if (!enablePlugin) return
const view = viewRef.current
queueMicrotask(() => addPlugin(view, nodeViewPlugin))
return () => queueMicrotask(() => removePlugin(view, nodeViewPlugin))
}, [enablePlugin])
// ...
}
``Follow our contribution guide to learn how to contribute to prosemirror-adapter.