import type { ReactNode } from 'react'
import { createContext, useContext, useMemo } from 'react'
import { EditorContext } from './Context.js'
import type { Editor, EditorContentProps, EditorStateSnapshot } from './index.js'
import { EditorContent, useEditorState } from './index.js'
/**
* The shape of the React context used by the `` components.
*
* The editor instance is always available when using the default `useEditor`
* configuration. For SSR scenarios where `immediatelyRender: false` is used,
* consider using the legacy `EditorProvider` pattern instead.
*/
export type TiptapContextType = {
/** The Tiptap editor instance. */
editor: Editor
}
/**
* React context that stores the current editor instance.
*
* Use `useTiptap()` to read from this context in child components.
*/
export const TiptapContext = createContext({
get editor(): Editor {
throw new Error('useTiptap must be used within a provider')
},
})
TiptapContext.displayName = 'TiptapContext'
/**
* Hook to read the Tiptap context and access the editor instance.
*
* This is a small convenience wrapper around `useContext(TiptapContext)`.
* The editor is always available when used within a `` provider.
*
* @returns The current `TiptapContextType` value from the provider.
*
* @example
* ```tsx
* import { useTiptap } from '@tiptap/react'
*
* function Toolbar() {
* const { editor } = useTiptap()
*
* return (
*
* )
* }
* ```
*/
export const useTiptap = () => useContext(TiptapContext)
/**
* Select a slice of the editor state using the context-provided editor.
*
* This is a thin wrapper around `useEditorState` that reads the `editor`
* instance from `useTiptap()` so callers don't have to pass it manually.
*
* @typeParam TSelectorResult - The type returned by the selector.
* @param selector - Function that receives the editor state snapshot and
* returns the piece of state you want to subscribe to.
* @param equalityFn - Optional function to compare previous/next selected
* values and avoid unnecessary updates.
* @returns The selected slice of the editor state.
*
* @example
* ```tsx
* function WordCount() {
* const wordCount = useTiptapState(state => {
* const text = state.editor.state.doc.textContent
* return text.split(/\s+/).filter(Boolean).length
* })
*
* return {wordCount} words
* }
* ```
*/
export function useTiptapState(
selector: (context: EditorStateSnapshot) => TSelectorResult,
equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean,
) {
const { editor } = useTiptap()
return useEditorState({
editor,
selector,
equalityFn,
})
}
/**
* Props for the `Tiptap` root/provider component.
*/
export type TiptapWrapperProps = {
/**
* The editor instance to provide to child components.
* Use `useEditor()` to create this instance.
*/
editor?: Editor
/**
* @deprecated Use `editor` instead. Will be removed in the next major version.
*/
instance?: Editor
children: ReactNode
}
/**
* Top-level provider component that makes the editor instance available via
* React context to all child components.
*
* This component also provides backwards compatibility with the legacy
* `EditorContext`, so components using `useCurrentEditor()` will work
* inside a `` provider.
*
* @param props - Component props.
* @returns A context provider element wrapping `children`.
*
* @example
* ```tsx
* import { Tiptap, useEditor } from '@tiptap/react'
*
* function App() {
* const editor = useEditor({ extensions: [...] })
*
* return (
*
*
*
*
* )
* }
* ```
*/
export function TiptapWrapper({ editor, instance, children }: TiptapWrapperProps) {
const resolvedEditor = editor ?? instance
if (!resolvedEditor) {
throw new Error('Tiptap: An editor instance is required. Pass a non-null `editor` prop.')
}
const tiptapContextValue = useMemo(() => ({ editor: resolvedEditor }), [resolvedEditor])
// Provide backwards compatibility with the legacy EditorContext
// so components using useCurrentEditor() work inside
const legacyContextValue = useMemo(() => ({ editor: resolvedEditor }), [resolvedEditor])
return (
{children}
)
}
TiptapWrapper.displayName = 'Tiptap'
/**
* Convenience component that renders `EditorContent` using the context-provided
* editor instance. Use this instead of manually passing the `editor` prop.
*
* @param props - All `EditorContent` props except `editor` and `ref`.
* @returns An `EditorContent` element bound to the context editor.
*
* @example
* ```tsx
* // inside a Tiptap provider
*
* ```
*/
export function TiptapContent({ ...rest }: Omit) {
const { editor } = useTiptap()
return
}
TiptapContent.displayName = 'Tiptap.Content'
/**
* Root `Tiptap` component. Use it as the provider for all child components.
*
* The exported object includes the `Content` subcomponent for rendering the
* editor content area.
*
* This component provides both the new `TiptapContext` (accessed via `useTiptap()`)
* and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for
* backwards compatibility.
*
* For bubble menus and floating menus, import them separately from
* `@tiptap/react/menus` to keep floating-ui as an optional dependency.
*
* @example
* ```tsx
* import { Tiptap, useEditor } from '@tiptap/react'
* import { BubbleMenu } from '@tiptap/react/menus'
*
* function App() {
* const editor = useEditor({ extensions: [...] })
*
* return (
*
*
*
*
*
*
* )
* }
* ```
*/
export const Tiptap = Object.assign(TiptapWrapper, {
/**
* The Tiptap Content component that renders the EditorContent with the editor instance from the context.
* @see TiptapContent
*/
Content: TiptapContent,
})
export default Tiptap