How to Handle TipTap Editor State Management with React Context in reactjs-tiptap-editor

The reactjs-tiptap-editor package isolates the TipTap editor instance inside a React Context provider so any component can read or control the editor via the useEditorInstance hook without prop-drilling.

Managing editor state across complex React component trees often creates maintenance headaches. The hunghg255/reactjs-tiptap-editor repository solves this by implementing a dedicated React Context architecture that wraps the TipTap editor, enabling fully reactive state management and cross-component communication.

Architecture Overview

The state management system in reactjs-tiptap-editor consists of three coordinated pieces that work together to provide editor access throughout your application.

RichTextProvider

The RichTextProvider component serves as the top-level wrapper that creates the context and injects the editor instance. Located in src/components/RichTextProvider.tsx (lines 44-45), this provider wraps your app (or a specific part of it) and supplies both the EditorContext from @tiptap/react and a custom event bus. This is the entry point for all editor state management.

EditorContext

EditorContext.Provider is the official TipTap context that holds the { editor } object. All TipTap-aware hooks—including useEditorInstance, useActive, and useAttributes—read the editor from this context. By storing the editor instance in React Context, state updates automatically propagate when the editor changes (e.g., selection changes, transaction updates), keeping your UI fully reactive.

ReactBusProvider

ReactBusProvider implements a lightweight mitt-based event bus for cross-component communication. Found in src/components/ReactBus.tsx (lines 7-8), this provider exposes emit and on helpers that any component can import via the useBus hook. This handles events like opening the slash dialog or emitting toolbar actions without coupling components directly to the editor context.

Data Flow Implementation

The editor state flows through your application in four distinct steps:

  1. Create the editor using useEditor from @tiptap/react in a parent component.
  2. Pass the editor to <RichTextProvider editor={editor}>, which stores it in EditorContext.
  3. Access the context—a BusContext (mitt instance) is made available through ReactBusProvider alongside the editor.
  4. Consume in descendants—child components call useEditorInstance() to invoke TipTap commands or useBus() to subscribe to custom events.

Because the editor lives in React Context, you avoid manually forwarding the editor through props while maintaining optimal performance. The provider renders only once, and child components subscribe to specific editor events via TipTap's built-in listeners, preventing unnecessary re-renders.

Practical Code Examples

Setting Up the Provider

Wrap your application or editor section with RichTextProvider after creating the editor instance with useEditor:

import { useEditor } from '@tiptap/react';
import { RichTextProvider } from 'reactjs-tiptap-editor';

export default function MyEditorPage() {
  const editor = useEditor({
    extensions: [],        // load any extensions you need
    content: '<p>Hello, world!</p>',
  });

  return (
    <RichTextProvider editor={editor!}>
      {/* All child components now have access to the editor */}
      <MyToolbar />
      <EditorContent />
    </RichTextProvider>
  );
}

This setup corresponds to the provider implementation in src/components/RichTextProvider.tsx that injects EditorContext into your component tree.

Consuming the Editor Instance

Access the editor in any child component using the useEditorInstance hook, which reads from the EditorContext created by RichTextProvider. This hook lives in src/store/editor.ts:

import { useEditorInstance } from '@/store/editor';
import { Button } from '@/components/ui/button';

export function MyToolbar() {
  const { editor } = useEditorInstance();

  const toggleBold = () => {
    editor?.chain().focus().toggleBold().run();
  };

  return (
    <Button onClick={toggleBold}>Bold</Button>
  );
}

Cross-Component Communication with the Event Bus

For decoupled communication between components (such as opening dialogs or toolbar actions), use the useBus hook from ReactBusProvider. The bus is provided by ReactBusProvider and enables emit/subscribe patterns:

Emitting events:

import { useBus } from '@/components/ReactBus';
import { Button } from '@/components/ui/button';

export function SlashDialogTrigger() {
  const { emit } = useBus();

  const openSlash = () => {
    emit('openSlashDialog');   // any component listening for this will react
  };

  return (
    <Button onClick={openSlash}>/</Button>
  );
}

Listening for events:

import { useEffect, useState } from 'react';
import { useBus } from '@/components/ReactBus';
import { Dialog } from '@/components/ui/dialog';

export function SlashDialog() {
  const { on, off } = useBus();
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const handler = () => setOpen(true);
    on('openSlashDialog', handler);
    return () => off('openSlashDialog', handler);
  }, [on, off]);

  return <Dialog open={open}>{/* dialog content */}</Dialog>;
}

The on and off methods are simple wrappers around the mitt instance, enabling decoupled communication without prop-drilling.

Key Source Files

Understanding these files helps you extend the editor state management for your own needs:

Summary

  • React Context isolation: The RichTextProvider in src/components/RichTextProvider.tsx creates a centralized editor state that eliminates prop-drilling.
  • Reactive updates: Storing the editor in EditorContext ensures automatic UI updates when editor state changes.
  • Decoupled communication: The ReactBusProvider (src/components/ReactBus.tsx) adds a mitt-based event bus for cross-component events without direct coupling.
  • Hook-based access: Use useEditorInstance() from src/store/editor.ts to execute commands, and useBus() for custom event emission.
  • Performance optimized: The architecture minimizes re-renders by leveraging TipTap's built-in listeners rather than React state for editor content changes.

Frequently Asked Questions

What is the role of RichTextProvider in reactjs-tiptap-editor?

RichTextProvider is the top-level component in src/components/RichTextProvider.tsx that instantiates the React Context structure. It accepts an editor prop created by useEditor and injects it into both EditorContext (for editor state) and BusContext (for events), making the editor instance available to all descendant components without manual prop passing.

How do I access the TipTap editor instance in a child component?

Import and call useEditorInstance() from @/store/editor (or the library's public exports). This hook wraps useContext(EditorContext) and returns the { editor } object, allowing you to invoke TipTap commands like editor?.chain().focus().toggleBold().run() directly from any component inside the RichTextProvider tree.

Why does the repository use a separate event bus alongside React Context?

The ReactBusProvider (using mitt from src/components/ReactBus.tsx) handles cross-component communication for UI-level events—such as opening dialogs or coordinating toolbar actions—that do not strictly depend on the editor's document state. This separation keeps EditorContext focused on the TipTap editor instance while the bus handles application-level events, improving modularity and reducing unnecessary re-renders on the editor context.

Can I use reactjs-tiptap-editor without React Context?

While the library is architected around React Context for state management, you could technically pass the editor instance directly through props. However, you would lose the benefits of the built-in RichTextProvider, useEditorInstance hook, and the event bus architecture, requiring you to reimplement much of the decoupling logic found in src/components/RichTextProvider.tsx and src/components/ReactBus.tsx.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →