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:
- Create the editor using
useEditorfrom@tiptap/reactin a parent component. - Pass the editor to
<RichTextProvider editor={editor}>, which stores it inEditorContext. - Access the context—a
BusContext(mittinstance) is made available throughReactBusProvideralongside the editor. - Consume in descendants—child components call
useEditorInstance()to invoke TipTap commands oruseBus()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:
src/components/RichTextProvider.tsx— Main provider that wraps the app, injectsEditorContextand the customReactBus.src/components/ReactBus.tsx— Implements themittevent bus and exportsBusContext,emit,useBus, andReactBusProvider.src/store/editor.ts— Re-exports theEditorContextand provides theuseEditorInstancehook used throughout the library.src/hooks/useExtension.tsx— Example hook that accesses the editor viauseEditorInstanceand registers extension side-effects.src/components/SlashDialogTrigger/SlashDialogTrigger.tsx— UI component that emits custom events to open the slash dialog, demonstrating practical bus usage.
Summary
- React Context isolation: The
RichTextProviderinsrc/components/RichTextProvider.tsxcreates a centralized editor state that eliminates prop-drilling. - Reactive updates: Storing the editor in
EditorContextensures automatic UI updates when editor state changes. - Decoupled communication: The
ReactBusProvider(src/components/ReactBus.tsx) adds amitt-based event bus for cross-component events without direct coupling. - Hook-based access: Use
useEditorInstance()fromsrc/store/editor.tsto execute commands, anduseBus()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →