Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove intermediate render cycle from useEditor #4579

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demos/src/Examples/Default/React/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,6 @@ display: none;

export default () => {
return (
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={content}></EditorProvider>
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={content} useImmediateRender></EditorProvider>
)
}
11 changes: 11 additions & 0 deletions docs/api/utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Editor API Utility Overview

Welcome to the Editor API Utility section. Here, you'll discover essential tools to enhance your Tiptap experience:

- **Render JSON as HTML**: Learn to convert JSON content to HTML, even without an editor instance, simplifying content management.

- **Tiptap for PHP**: Explore PHP integration for Tiptap, enabling seamless content transformation and modification.

- **Suggestions**: Enhance your editor with suggestions like mentions and emojis, tailored to your needs.

Explore subpages for in-depth guidance and examples.
3 changes: 1 addition & 2 deletions docs/links.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,7 @@
link: /api/extensions/unique-id
type: pro
- title: Utilities
link: /utilities
redirect: /api/utilities/html
link: /api/utilities
items:
- title: HTML
link: /api/utilities/html
Expand Down
44 changes: 38 additions & 6 deletions packages/react/src/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { createContext, ReactNode, useContext } from 'react'

import { Editor } from './Editor.js'
import { EditorContent } from './EditorContent.js'
import { useEditor } from './useEditor.js'
import { useEditor, useEditorForImmediateRender } from './useEditor.js'

export type EditorContextValue = {
editor: Editor | null;
Expand All @@ -19,18 +19,38 @@ export const useCurrentEditor = () => useContext(EditorContext)

export type EditorProviderProps = {
children: ReactNode;
/**
* This option will create and immediately return a defined editor instance. The editor returned in the context consumer will never be null if
* this is enabled. In future major versions, this property will be removed and this behavior will be the defualt.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo defualt to default

*/
useImmediateRender?: boolean;
slotBefore?: ReactNode;
slotAfter?: ReactNode;
} & Partial<EditorOptions>

export const EditorProvider = ({
const EditorProviderNoImmediateRender = ({
children, slotAfter, slotBefore, ...editorOptions
}: EditorProviderProps) => {
}: Omit<EditorProviderProps, 'useImmediateRender'>) => {
const editor = useEditor(editorOptions)

if (!editor) {
return null
}
return (
<EditorContext.Provider value={{ editor }}>
{slotBefore}
<EditorConsumer>
{({ editor: currentEditor }) => (
<EditorContent editor={currentEditor} />
)}
</EditorConsumer>
{children}
{slotAfter}
</EditorContext.Provider>
)
}

const EditorProviderImmediateRender = ({
children, slotAfter, slotBefore, ...editorOptions
}: Omit<EditorProviderProps, 'useImmediateRender'>) => {
const editor = useEditorForImmediateRender(editorOptions)

return (
<EditorContext.Provider value={{ editor }}>
Expand All @@ -45,3 +65,15 @@ export const EditorProvider = ({
</EditorContext.Provider>
)
}

export const EditorProvider = ({ useImmediateRender, ...providerOptions }: EditorProviderProps) => {
if (useImmediateRender) {
return (
<EditorProviderImmediateRender {...providerOptions} />
)
}

return (
<EditorProviderNoImmediateRender {...providerOptions} />
)
}
121 changes: 119 additions & 2 deletions packages/react/src/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EditorOptions } from '@tiptap/core'
import {
DependencyList,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
Expand Down Expand Up @@ -113,14 +114,130 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency

return () => {
isMounted = false
editorRef.current?.destroy()
C-Hess marked this conversation as resolved.
Show resolved Hide resolved
}
}, deps)

return editorRef.current
}

/**
* This hook will create and immediately return a defined editor instance. In future major versions, this
* hook will be removed and useEditor will be changed to behave like this hook.
*/
export const useEditorForImmediateRender = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
const {
onBeforeCreate,
onBlur,
onCreate,
onDestroy,
onFocus,
onSelectionUpdate,
onTransaction,
onUpdate,
} = options

const onBeforeCreateRef = useRef(onBeforeCreate)
const onBlurRef = useRef(onBlur)
const onCreateRef = useRef(onCreate)
const onDestroyRef = useRef(onDestroy)
const onFocusRef = useRef(onFocus)
const onSelectionUpdateRef = useRef(onSelectionUpdate)
const onTransactionRef = useRef(onTransaction)
const onUpdateRef = useRef(onUpdate)

const isMounted = useRef(false)

useEffect(() => {
isMounted.current = true

return () => {
return editorRef.current?.destroy()
isMounted.current = false
}
}, [])

return editorRef.current
const [, forceUpdate] = useState({})
const editor = useMemo(() => {
const instance = new Editor(options)

instance.on('transaction', () => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (isMounted.current) {
forceUpdate({})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required in this PR, but it would be nice to add comment here why is this section necessary (to call the forceUpdate wrapped in 2 requestAnimationFrame calls on every editor transaction). This is a bit black magic to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. I just copied this over from the original hook, but it looks to me like it's logic to get around timing issues between Prosemirror transactions and React render cycles

Copy link

@szymonchudy szymonchudy May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this will help, but this seems to fix the flickering issue mentioned here: #2040

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is a habit in this repo of closing non-fixed issues.

@szymonchudy is that issue separate from #5166? It's hard to tell if they're related or truly the same.

}
})
})
})

return instance
}, deps)

useEffect(() => {
return () => {
editor.destroy()
}
}, [editor])

// This effect will handle updating the editor instance
// when the event handlers change.
useEffect(() => {
if (onBeforeCreate) {
editor.off('beforeCreate', onBeforeCreateRef.current)
editor.on('beforeCreate', onBeforeCreate)

onBeforeCreateRef.current = onBeforeCreate
}

if (onBlur) {
editor.off('blur', onBlurRef.current)
editor.on('blur', onBlur)

onBlurRef.current = onBlur
}

if (onCreate) {
editor.off('create', onCreateRef.current)
editor.on('create', onCreate)

onCreateRef.current = onCreate
}

if (onDestroy) {
editor.off('destroy', onDestroyRef.current)
editor.on('destroy', onDestroy)

onDestroyRef.current = onDestroy
}

if (onFocus) {
editor.off('focus', onFocusRef.current)
editor.on('focus', onFocus)

onFocusRef.current = onFocus
}

if (onSelectionUpdate) {
editor.off('selectionUpdate', onSelectionUpdateRef.current)
editor.on('selectionUpdate', onSelectionUpdate)

onSelectionUpdateRef.current = onSelectionUpdate
}

if (onTransaction) {
editor.off('transaction', onTransactionRef.current)
editor.on('transaction', onTransaction)

onTransactionRef.current = onTransaction
}

if (onUpdate) {
editor.off('update', onUpdateRef.current)
editor.on('update', onUpdate)

onUpdateRef.current = onUpdate
}
}, [onBeforeCreate, onBlur, onCreate, onDestroy, onFocus, onSelectionUpdate, onTransaction, onUpdate, editor])

return editor
}
Loading