Using Tiptap with the React Composable API

Editor

Tiptap provides a declarative <Tiptap> component that simplifies editor setup and automatically provides context to all child components. This composable API is an alternative to the hook-based useEditor + <EditorContent /> approach, offering a more React-idiomatic way to work with Tiptap.

When to use the Composable API

The composable API is ideal when you:

  • Want a more declarative, component-based approach
  • Need to access the editor from multiple child components
  • Prefer automatic context management over manual prop passing
  • Want built-in loading states and SSR-friendly patterns

For simpler use cases or when you need more direct control, the hook-based useEditor approach may be more appropriate.

Installation

Before using the composable API, make sure you have Tiptap installed in your React project. Follow the React installation guide to set up the required dependencies.

Using the Tiptap component

The <Tiptap> component is the root provider that makes the editor instance available to all child components via React context.

Basic setup

Create a new component and import the Tiptap component along with useEditor:

// src/Editor.tsx
import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>Loading editor...</Tiptap.Loading>
      <MenuBar />
      <Tiptap.Content />
      <Tiptap.BubbleMenu>
        <button>Bold</button>
        <button>Italic</button>
      </Tiptap.BubbleMenu>
      <Tiptap.FloatingMenu>
        <button>Add heading</button>
      </Tiptap.FloatingMenu>
    </Tiptap>
  )
}

export default Editor

Available subcomponents

The <Tiptap> component provides several subcomponents that handle common editor UI patterns:

ComponentDescription
Tiptap.ContentRenders the editor content area. Replaces <EditorContent editor={editor} />.
Tiptap.LoadingRenders its children only while the editor is initializing. Useful for loading states.
Tiptap.BubbleMenuA context-aware bubble menu that appears on text selection.
Tiptap.FloatingMenuA context-aware floating menu that appears on empty lines.

Accessing the editor in child components

One of the main benefits of the composable API is that child components can access the editor without prop drilling.

Using the useTiptap hook

The useTiptap hook returns the editor instance and an isReady flag that indicates whether the editor has finished initializing.

import { useTiptap } from '@tiptap/react'

function MenuBar() {
  const { editor, isReady } = useTiptap()

  if (!isReady || !editor) {
    return null
  }

  return (
    <div className="menu-bar">
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'is-active' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editor.isActive('italic') ? 'is-active' : ''}
      >
        Italic
      </button>
    </div>
  )
}

Then include the menu bar anywhere inside your <Tiptap> component:

<Tiptap instance={editor}>
  <MenuBar />
  <Tiptap.Content />
</Tiptap>

Using useTiptapState for reactive state

For performance-sensitive components, use useTiptapState to subscribe to specific parts of the editor state. This prevents unnecessary re-renders when unrelated state changes.

import { useTiptap, useTiptapState } from '@tiptap/react'

function WordCount() {
  const { isReady } = useTiptap()

  const wordCount = useTiptapState((state) => {
    const text = state.editor.state.doc.textContent
    return text.split(/\s+/).filter(Boolean).length
  })

  if (!isReady) {
    return null
  }

  return <span>{wordCount} words</span>
}

The selector function receives an EditorStateSnapshot and should return only the data your component needs. The component will only re-render when the selected value changes.

Important

Only use useTiptapState when the editor is ready. Check isReady from useTiptap() before rendering components that use this hook.

Server-side rendering (SSR)

The composable API works seamlessly with server-side rendering. Use the immediatelyRender option to prevent the editor from rendering on the server, and leverage Tiptap.Loading to display a placeholder:

'use client'

import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

export function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
    immediatelyRender: false,
  })

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>
        <div className="skeleton">Loading editor...</div>
      </Tiptap.Loading>
      <Tiptap.Content />
    </Tiptap>
  )
}

The Tiptap.Loading component is particularly useful with SSR as it displays a placeholder until the editor initializes on the client.

Performance considerations

The composable API is designed with performance in mind:

  • Automatic context optimization: The editor context is memoized to prevent unnecessary re-renders
  • Selective subscriptions: Use useTiptapState to subscribe only to the state you need
  • Built-in loading states: Prevent rendering child components until the editor is ready

For more performance tips, see the React Performance Guide.

Backwards compatibility

The <Tiptap> component automatically provides the EditorContext, which means you can use the useCurrentEditor hook inside it for backwards compatibility with existing code:

import { useCurrentEditor } from '@tiptap/react'

function EditorJSONPreview() {
  const { editor } = useCurrentEditor()

  if (!editor) {
    return null
  }

  return <pre>{JSON.stringify(editor.getJSON(), null, 2)}</pre>
}

However, for new code, we recommend using useTiptap() which provides additional context like the isReady flag.

API Reference

Tiptap component

The root provider component that makes the editor instance available via React context.

Props:

PropTypeDescription
instanceEditor | nullThe editor instance from useEditor()
childrenReactNodeChild components

Example:

<Tiptap instance={editor}>
  <Tiptap.Content />
</Tiptap>

useTiptap hook

Returns the Tiptap context value.

Returns:

PropertyTypeDescription
editorEditor | nullThe editor instance
isReadybooleantrue when the editor has finished initializing

Example:

const { editor, isReady } = useTiptap()

if (!isReady || !editor) {
  return null
}

useTiptapState hook

Subscribes to a slice of the editor state using a selector function.

Signature:

const value = useTiptapState(selector, equalityFn?)

Parameters:

ParameterTypeDescription
selector(state: EditorStateSnapshot) => TFunction to select state
equalityFn(a: T, b: T) => booleanOptional equality function to control re-renders. Defaults to deepEqual from fast-equals (deep value comparison).

Example:

const isBold = useTiptapState((state) => state.editor.isActive('bold'))

Comparison: Composable API vs Hook-based Approach

FeatureComposable APIHook-based Approach
Setup complexityLow - declarative componentsMedium - manual prop passing
Context managementAutomaticManual via EditorContext.Provider
Child component accessEasy via useTiptap()Requires prop drilling or context
Loading statesBuilt-in Tiptap.LoadingManual implementation
SSR supportBuilt-in with Tiptap.LoadingRequires manual null checks
PerformanceOptimized with useTiptapStateOptimized with useEditorState
Best forComplex UIs with many child componentsSimple UIs or direct control needed

Next steps