@tiptap/react 3.18.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Tiptap.tsx CHANGED
@@ -1,41 +1,40 @@
1
1
  import type { ReactNode } from 'react'
2
- import { createContext, useContext, useEffect, useMemo, useState } from 'react'
2
+ import { createContext, useContext, useMemo } from 'react'
3
3
 
4
4
  import { EditorContext } from './Context.js'
5
5
  import type { Editor, EditorContentProps, EditorStateSnapshot } from './index.js'
6
6
  import { EditorContent, useEditorState } from './index.js'
7
- import { type BubbleMenuProps, BubbleMenu } from './menus/BubbleMenu.js'
8
- import { type FloatingMenuProps, FloatingMenu } from './menus/FloatingMenu.js'
9
7
 
10
8
  /**
11
9
  * The shape of the React context used by the `<Tiptap />` components.
12
10
  *
13
- * This object exposes the editor instance and a simple readiness flag.
11
+ * The editor instance is always available when using the default `useEditor`
12
+ * configuration. For SSR scenarios where `immediatelyRender: false` is used,
13
+ * consider using the legacy `EditorProvider` pattern instead.
14
14
  */
15
15
  export type TiptapContextType = {
16
- /** The Tiptap editor instance. May be null during SSR or before initialization. */
17
- editor: Editor | null
18
-
19
- /** True when the editor has finished initializing and is ready for user interaction. */
20
- isReady: boolean
16
+ /** The Tiptap editor instance. */
17
+ editor: Editor
21
18
  }
22
19
 
23
20
  /**
24
- * React context that stores the current editor instance and readiness flag.
21
+ * React context that stores the current editor instance.
25
22
  *
26
23
  * Use `useTiptap()` to read from this context in child components.
27
24
  */
28
25
  export const TiptapContext = createContext<TiptapContextType>({
29
- editor: null,
30
- isReady: false,
26
+ get editor(): Editor {
27
+ throw new Error('useTiptap must be used within a <Tiptap> provider')
28
+ },
31
29
  })
32
30
 
33
31
  TiptapContext.displayName = 'TiptapContext'
34
32
 
35
33
  /**
36
- * Hook to read the Tiptap context (`editor` + `isReady`).
34
+ * Hook to read the Tiptap context and access the editor instance.
37
35
  *
38
36
  * This is a small convenience wrapper around `useContext(TiptapContext)`.
37
+ * The editor is always available when used within a `<Tiptap>` provider.
39
38
  *
40
39
  * @returns The current `TiptapContextType` value from the provider.
41
40
  *
@@ -43,9 +42,14 @@ TiptapContext.displayName = 'TiptapContext'
43
42
  * ```tsx
44
43
  * import { useTiptap } from '@tiptap/react'
45
44
  *
46
- * function Status() {
47
- * const { isReady } = useTiptap()
48
- * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div>
45
+ * function Toolbar() {
46
+ * const { editor } = useTiptap()
47
+ *
48
+ * return (
49
+ * <button onClick={() => editor.chain().focus().toggleBold().run()}>
50
+ * Bold
51
+ * </button>
52
+ * )
49
53
  * }
50
54
  * ```
51
55
  */
@@ -57,10 +61,6 @@ export const useTiptap = () => useContext(TiptapContext)
57
61
  * This is a thin wrapper around `useEditorState` that reads the `editor`
58
62
  * instance from `useTiptap()` so callers don't have to pass it manually.
59
63
  *
60
- * Important: This hook should only be used when the editor is available.
61
- * Use the `isReady` flag from `useTiptap()` to guard against null editor,
62
- * or ensure your component only renders after the editor is initialized.
63
- *
64
64
  * @typeParam TSelectorResult - The type returned by the selector.
65
65
  * @param selector - Function that receives the editor state snapshot and
66
66
  * returns the piece of state you want to subscribe to.
@@ -71,16 +71,11 @@ export const useTiptap = () => useContext(TiptapContext)
71
71
  * @example
72
72
  * ```tsx
73
73
  * function WordCount() {
74
- * const { isReady } = useTiptap()
75
- *
76
- * // Only use useTiptapState when the editor is ready
77
74
  * const wordCount = useTiptapState(state => {
78
75
  * const text = state.editor.state.doc.textContent
79
76
  * return text.split(/\s+/).filter(Boolean).length
80
77
  * })
81
78
  *
82
- * if (!isReady) return null
83
- *
84
79
  * return <span>{wordCount} words</span>
85
80
  * }
86
81
  * ```
@@ -90,8 +85,9 @@ export function useTiptapState<TSelectorResult>(
90
85
  equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean,
91
86
  ) {
92
87
  const { editor } = useTiptap()
88
+
93
89
  return useEditorState({
94
- editor: editor as Editor,
90
+ editor,
95
91
  selector,
96
92
  equalityFn,
97
93
  })
@@ -103,18 +99,21 @@ export function useTiptapState<TSelectorResult>(
103
99
  export type TiptapWrapperProps = {
104
100
  /**
105
101
  * The editor instance to provide to child components.
106
- * Can be null during SSR or before initialization.
102
+ * Use `useEditor()` to create this instance.
103
+ */
104
+ editor?: Editor
105
+
106
+ /**
107
+ * @deprecated Use `editor` instead. Will be removed in the next major version.
107
108
  */
108
- instance: Editor | null
109
+ instance?: Editor
110
+
109
111
  children: ReactNode
110
112
  }
111
113
 
112
114
  /**
113
115
  * Top-level provider component that makes the editor instance available via
114
- * React context and tracks when the editor becomes ready.
115
- *
116
- * The component listens to the editor's `create` event and flips the
117
- * `isReady` flag once initialization completes.
116
+ * React context to all child components.
118
117
  *
119
118
  * This component also provides backwards compatibility with the legacy
120
119
  * `EditorContext`, so components using `useCurrentEditor()` will work
@@ -131,7 +130,7 @@ export type TiptapWrapperProps = {
131
130
  * const editor = useEditor({ extensions: [...] })
132
131
  *
133
132
  * return (
134
- * <Tiptap instance={editor}>
133
+ * <Tiptap editor={editor}>
135
134
  * <Toolbar />
136
135
  * <Tiptap.Content />
137
136
  * </Tiptap>
@@ -139,38 +138,18 @@ export type TiptapWrapperProps = {
139
138
  * }
140
139
  * ```
141
140
  */
142
- export function TiptapWrapper({ instance, children }: TiptapWrapperProps) {
143
- const [isReady, setIsReady] = useState(instance?.isInitialized ?? false)
144
-
145
- useEffect(() => {
146
- if (!instance) {
147
- setIsReady(false)
148
- return
149
- }
150
-
151
- // If the editor is already initialized, set isReady to true
152
- if (instance.isInitialized) {
153
- setIsReady(true)
154
- return
155
- }
156
-
157
- const handleCreate = () => {
158
- setIsReady(true)
159
- }
160
-
161
- instance.on('create', handleCreate)
141
+ export function TiptapWrapper({ editor, instance, children }: TiptapWrapperProps) {
142
+ const resolvedEditor = editor ?? instance
162
143
 
163
- return () => {
164
- instance.off('create', handleCreate)
165
- }
166
- }, [instance])
144
+ if (!resolvedEditor) {
145
+ throw new Error('Tiptap: An editor instance is required. Pass a non-null `editor` prop.')
146
+ }
167
147
 
168
- // Memoize context values to prevent unnecessary re-renders
169
- const tiptapContextValue = useMemo<TiptapContextType>(() => ({ editor: instance, isReady }), [instance, isReady])
148
+ const tiptapContextValue = useMemo<TiptapContextType>(() => ({ editor: resolvedEditor }), [resolvedEditor])
170
149
 
171
150
  // Provide backwards compatibility with the legacy EditorContext
172
151
  // so components using useCurrentEditor() work inside <Tiptap>
173
- const legacyContextValue = useMemo(() => ({ editor: instance }), [instance])
152
+ const legacyContextValue = useMemo(() => ({ editor: resolvedEditor }), [resolvedEditor])
174
153
 
175
154
  return (
176
155
  <EditorContext.Provider value={legacyContextValue}>
@@ -202,128 +181,36 @@ export function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | '
202
181
 
203
182
  TiptapContent.displayName = 'Tiptap.Content'
204
183
 
205
- export type TiptapLoadingProps = {
206
- children: ReactNode
207
- }
208
-
209
- /**
210
- * Component that renders its children only when the editor is not ready.
211
- *
212
- * This is useful for displaying loading states or placeholders during
213
- * editor initialization, especially with SSR.
214
- *
215
- * @param props - The props for the TiptapLoading component.
216
- * @returns The children when editor is not ready, or null when ready.
217
- *
218
- * @example
219
- * ```tsx
220
- * <Tiptap instance={editor}>
221
- * <Tiptap.Loading>
222
- * <div className="skeleton">Loading editor...</div>
223
- * </Tiptap.Loading>
224
- * <Tiptap.Content />
225
- * </Tiptap>
226
- * ```
227
- */
228
- export function TiptapLoading({ children }: TiptapLoadingProps) {
229
- const { isReady } = useTiptap()
230
-
231
- if (isReady) {
232
- return null
233
- }
234
-
235
- return children
236
- }
237
-
238
- TiptapLoading.displayName = 'Tiptap.Loading'
239
-
240
- /**
241
- * A wrapper around the library `BubbleMenu` that injects the editor from
242
- * context so callers don't need to pass the `editor` prop.
243
- *
244
- * Returns `null` when the editor is not available (for example during SSR).
245
- *
246
- * @param props - Props for the underlying `BubbleMenu` (except `editor`).
247
- * @returns A `BubbleMenu` bound to the context editor, or `null`.
248
- *
249
- * @example
250
- * ```tsx
251
- * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}>
252
- * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
253
- * </Tiptap.BubbleMenu>
254
- * ```
255
- */
256
- export function TiptapBubbleMenu({ children, ...rest }: { children: ReactNode } & Omit<BubbleMenuProps, 'editor'>) {
257
- const { editor } = useTiptap()
258
-
259
- if (!editor) {
260
- return null
261
- }
262
-
263
- return (
264
- <BubbleMenu editor={editor} {...rest}>
265
- {children}
266
- </BubbleMenu>
267
- )
268
- }
269
-
270
- TiptapBubbleMenu.displayName = 'Tiptap.BubbleMenu'
271
-
272
- /**
273
- * A wrapper around the library `FloatingMenu` that injects the editor from
274
- * context so callers don't need to pass the `editor` prop.
275
- *
276
- * Returns `null` when the editor is not available.
277
- *
278
- * @param props - Props for the underlying `FloatingMenu` (except `editor`).
279
- * @returns A `FloatingMenu` bound to the context editor, or `null`.
280
- *
281
- * @example
282
- * ```tsx
283
- * <Tiptap.FloatingMenu placement="top">
284
- * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button>
285
- * </Tiptap.FloatingMenu>
286
- * ```
287
- */
288
- export function TiptapFloatingMenu({ children, ...rest }: { children: ReactNode } & Omit<FloatingMenuProps, 'editor'>) {
289
- const { editor } = useTiptap()
290
-
291
- if (!editor) {
292
- return null
293
- }
294
-
295
- return (
296
- <FloatingMenu {...rest} editor={editor}>
297
- {children}
298
- </FloatingMenu>
299
- )
300
- }
301
-
302
- TiptapFloatingMenu.displayName = 'Tiptap.FloatingMenu'
303
-
304
184
  /**
305
185
  * Root `Tiptap` component. Use it as the provider for all child components.
306
186
  *
307
- * The exported object includes several helper subcomponents for common use
308
- * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`.
187
+ * The exported object includes the `Content` subcomponent for rendering the
188
+ * editor content area.
309
189
  *
310
190
  * This component provides both the new `TiptapContext` (accessed via `useTiptap()`)
311
191
  * and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for
312
192
  * backwards compatibility.
313
193
  *
194
+ * For bubble menus and floating menus, import them separately from
195
+ * `@tiptap/react/menus` to keep floating-ui as an optional dependency.
196
+ *
314
197
  * @example
315
198
  * ```tsx
316
- * const editor = useEditor({ extensions: [...] })
199
+ * import { Tiptap, useEditor } from '@tiptap/react'
200
+ * import { BubbleMenu } from '@tiptap/react/menus'
317
201
  *
318
- * return (
319
- * <Tiptap instance={editor}>
320
- * <Tiptap.Loading>Initializing editor...</Tiptap.Loading>
321
- * <Tiptap.Content />
322
- * <Tiptap.BubbleMenu>
323
- * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
324
- * </Tiptap.BubbleMenu>
325
- * </Tiptap>
326
- * )
202
+ * function App() {
203
+ * const editor = useEditor({ extensions: [...] })
204
+ *
205
+ * return (
206
+ * <Tiptap editor={editor}>
207
+ * <Tiptap.Content />
208
+ * <BubbleMenu>
209
+ * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
210
+ * </BubbleMenu>
211
+ * </Tiptap>
212
+ * )
213
+ * }
327
214
  * ```
328
215
  */
329
216
  export const Tiptap = Object.assign(TiptapWrapper, {
@@ -332,24 +219,6 @@ export const Tiptap = Object.assign(TiptapWrapper, {
332
219
  * @see TiptapContent
333
220
  */
334
221
  Content: TiptapContent,
335
-
336
- /**
337
- * The Tiptap Loading component that renders its children only when the editor is not ready.
338
- * @see TiptapLoading
339
- */
340
- Loading: TiptapLoading,
341
-
342
- /**
343
- * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context.
344
- * @see TiptapBubbleMenu
345
- */
346
- BubbleMenu: TiptapBubbleMenu,
347
-
348
- /**
349
- * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context.
350
- * @see TiptapFloatingMenu
351
- */
352
- FloatingMenu: TiptapFloatingMenu,
353
222
  })
354
223
 
355
224
  export default Tiptap