React Tiptap Editor Integration

When using Tiptap with React, the most common performance issue is that the editor is re-rendered too often. This demo shows the difference between the default editor and the optimized rendering with shouldRerenderOnTransaction.

Preview

React

Optimize rendering

Render every transaction (default behavior)

Toggle rendered

Number of renders: 1

A highly optimized editor that only re-renders when it’s necessary.

import './styles.scss'

import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
import { BubbleMenu } from '@tiptap/react/menus'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'

function EditorInstance({ shouldOptimizeRendering }) {
  const countRenderRef = React.useRef(0)

countRenderRef.current += 1

const editor = useEditor({
    /**
     * This option gives us the control to enable the default behavior of rendering the editor immediately.
     */
    immediatelyRender: true,
    /**
     * This option gives us the control to disable the default behavior of re-rendering the editor on every transaction.
     */
    shouldRerenderOnTransaction: !shouldOptimizeRendering,
    extensions: [StarterKit],
    content: `
    <p>
      A highly optimized editor that only re-renders when it’s necessary.
    </p>
    `,
  })
  /**
   * This hook allows us to select the editor state we want to use in our component.
   */
  const currentEditorState = useEditorState({
    /**
     * The editor instance we want to use.
     */
    editor,
    /**
     * This selector allows us to select the data we want to use in our component.
     * It is evaluated on every editor transaction and compared to it's previously returned value.
     */
    selector: ctx => ({
      isBold: ctx.editor.isActive('bold'),
      isItalic: ctx.editor.isActive('italic'),
      isStrike: ctx.editor.isActive('strike'),
    }),
    /**
     * This function allows us to customize the equality check for the selector.
     * By default it is a `===` check.
     */
    equalityFn: (prev, next) => {
      if (!next) {
        return false
      }
      return prev.isBold === next.isBold && prev.isItalic === next.isItalic && prev.isStrike === next.isStrike
    },
  })

return (
    <div>
      <div className="control-group">
        <div>
          Number of renders: <span id="render-count">{countRenderRef.current}</span>
        </div>
      </div>
      {currentEditorState && (
        <BubbleMenu className="bubble-menu" editor={editor}>
          <button
            onClick={() => editor.chain().focus().toggleBold().run()}
            className={currentEditorState.isBold ? 'is-active' : ''}
          >
            Bold
          </button>
          <button
            onClick={() => editor.chain().focus().toggleItalic().run()}
            className={currentEditorState.isItalic ? 'is-active' : ''}
          >
            Italic
          </button>
          <button
            onClick={() => editor.chain().focus().toggleStrike().run()}
            className={currentEditorState.isStrike ? 'is-active' : ''}
          >
            Strike
          </button>
        </BubbleMenu>
      )}
      <EditorContent editor={editor} />
    </div>
  )
}

const EditorControls = () => {
  const [shouldOptimizeRendering, setShouldOptimizeRendering] = React.useState(true)
  const [rendered, setRendered] = React.useState(true)

return (
    <>
      <div className="control-group">
        <div className="switch-group">
          <label>
            <input
              type="radio"
              name="option-switch"
              onChange={() => {
                setShouldOptimizeRendering(true)
              }}
              checked={shouldOptimizeRendering === true}
            />
            Optimize rendering
          </label>
          <label>
            <input
              type="radio"
              name="option-switch"
              onChange={() => {
                setShouldOptimizeRendering(false)
              }}
              checked={shouldOptimizeRendering === false}
            />
            Render every transaction (default behavior)
          </label>
        </div>
        <button onClick={() => setRendered(a => !a)}>Toggle rendered</button>
      </div>
      {rendered && <EditorInstance shouldOptimizeRendering={shouldOptimizeRendering} />}
    </>
  )
}

export default () => {
  return (
    <React.StrictMode>
      <EditorControls />
    </React.StrictMode>
  )
}

Get editor state information without impacting performance

If you need to access the editor state without causing a re-render, you can use the useEditorState hook. This allows you to read the editor state without triggering a re-render of your component.

Read more about it here.