tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

onUpdate callback does not update after re-render

Open ospfranco opened this issue 2 years ago • 19 comments

What’s the bug you are facing?

I have a wrapper around tiptap, whenever the text changes I trigger a request to my back-end, this works fine the first time tiptap is mounted but the parent component (where tiptap is mounted) can change its internal variables, and therefore the closure should capture a new context, the problem is that it doesn't after the parent component changes the state, the closure/lambda passed on the onUpdate function remains the same and therefore tiptap tries to update the wrong component.

Here is some of the code, my high level component on the parent, notice the id param, which is the param that changes at some point:

<Tiptap
  onFocus={({ editor }) => editor.commands.blur()}
  initialContent={project.notes ? JSON.parse(project.notes) : null}
  placeholder="You can add a default checklist in the settings."
  className="md:max-w-2xl lg:max-w-none"
  onChange={async (e) => {
    console.warn("URL PARAM ID", id) // ALWAYS REMAINS THE SAME, THEREFORE CANNOT UPDATE THE PROJECT CORRECTLY

    await updateProjectMutation({
      id,
      notes: JSON.stringify(e),
    })
    refetch()
  }}
  ref={tiptapRef}
/>

My internal TIptap implementation, notice the onUpdate function that I'm passing to the useEditor hook:

import Link from "@tiptap/extension-link"
import Placeholder from "@tiptap/extension-placeholder"
import TaskItem from "@tiptap/extension-task-item"
import TaskList from "@tiptap/extension-task-list"
import { BubbleMenu, EditorContent, Extension, useEditor } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import React, { forwardRef, useImperativeHandle, useState } from "react"
import { useBoolean } from "../hooks/useBoolean"
import { Button } from "./Button"

interface IProps {
  editable?: boolean
  onClick?: (this: unknown, view: any, pos: number, event: MouseEvent) => boolean
  initialContent?: any
  // content?: any
  onChange?: (content: any) => void
  autofocus?: boolean | null | "end" | "start"
  onFocus?: (params: { editor: any }) => void
  placeholder?: string
  className?: string
}

export const Tiptap = forwardRef<any, IProps>(
  (
    {
      editable = true,
      onClick,
      initialContent,
      onChange,
      autofocus,
      onFocus,
      placeholder,
      className,
      // content,
    },
    ref
  ) => {
    const [isAddingLink, addLinkOn, addLinkOff] = useBoolean()
    const [link, setLink] = useState("")
    const editor = useEditor({
      autofocus,
      onFocus: onFocus ? onFocus : () => {},
      editorProps: {
        attributes: {
          class: "prose focus:outline-none dark:prose-dark dark:text-gray-300 text-base",
        },
        editable: () => editable,
        handleClick: onClick,
      },
      content: initialContent,
      onUpdate: ({ editor }) => {
        onChange?.(editor.getJSON())
      },
      extensions: [
        StarterKit,
        Placeholder.configure({
          showOnlyWhenEditable: false,
          placeholder,
        }),
        TaskList.configure({
          HTMLAttributes: {
            class: "pl-0",
          },
        }),
        TaskItem.configure({
          HTMLAttributes: {
            class: "before:hidden pl-0 flex items-center dumb-prose-remove",
          },
        }),
        Extension.create({
          // Do not insert line break when pressing CMD+Enter
          // Most of the time handled by upper components
          addKeyboardShortcuts() {
            return {
              "Cmd-Enter"() {
                return true
              },
              "Ctrl-Enter"() {
                return true
              },
            }
          },
        }),
        Link,
      ],
    })

    useImperativeHandle(ref, () => ({
      getEditorInstance() {
        return editor
      },
    }))


    return (
        <EditorContent editor={editor} className={className} />
    )
  }
)

In any case, it seems the useEditor hook saves only the first passed onUpdate function and does not update it in sub-sequent renders

How can we reproduce the bug on our side?

Attached the code above, but if necessary I can try to reproduce the issue in a code sandbox

Can you provide a CodeSandbox?

No response

What did you expect to happen?

The passed callback onUpdate should be updated when a new value is passed to it, instead of constantly re-using the first memoized value

Anything to add? (optional)

I tried to update tiptap to the latest version but then I faced this other crash: https://github.com/ueberdosis/tiptap/issues/577 so I reverted to my old/current versions

"@tiptap/extension-bubble-menu": "2.0.0-beta.51",
    "@tiptap/extension-link": "2.0.0-beta.33",
    "@tiptap/extension-placeholder": "2.0.0-beta.45",
    "@tiptap/extension-task-item": "2.0.0-beta.30",
    "@tiptap/extension-task-list": "2.0.0-beta.24",
    "@tiptap/react": "2.0.0-beta.98",
    "@tiptap/starter-kit": "2.0.0-beta.154",

Did you update your dependencies?

  • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

Are you sponsoring us?

  • [ ] Yes, I’m a sponsor. 💖

ospfranco avatar Jan 18 '22 18:01 ospfranco

ok, it seems like I've been using the useEditor hook very wrong, as you can see I was using an imperative handle to try to set the content of the editor after a parent re-render, but everything needs to go as a dependency in the useEditor hook and tiptap should re-render itself...

Just as a comment, the examples on the documentation page should mention this more clearly

Anyways, sorry for the confusion and thanks for the package!

ospfranco avatar Jan 18 '22 18:01 ospfranco

Facing new issues now, here is the updated version of my component:

import Link from "@tiptap/extension-link"
import Placeholder from "@tiptap/extension-placeholder"
import TaskItem from "@tiptap/extension-task-item"
import TaskList from "@tiptap/extension-task-list"
import { EditorContent, EditorEvents, Extension } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import React, { FC, memo, useState } from "react"
import { useBoolean } from "../hooks/useBoolean"
import { useEditor } from "../hooks/useEditor"

interface IProps {
  editable?: boolean
  // onClick?: (this: unknown, view: any, pos: number, event: MouseEvent) => boolean
  content?: any
  onUpdate?: (params: EditorEvents["update"]) => void
  autofocus?: boolean | null | "end" | "start"
  onFocus?: (params: EditorEvents["focus"]) => void
  placeholder?: string
  className?: string
}

export const Tiptap: FC<IProps> = memo(
  ({ editable = true, content, onUpdate, autofocus, onFocus, placeholder, className }) => {
    console.warn("tiptap render content", content)

    const [isAddingLink, addLinkOn, addLinkOff] = useBoolean()
    const [link, setLink] = useState("")
    const editor = useEditor(
      {
        autofocus,
        // onFocus: onFocus ? onFocus : () => {},
        editorProps: {
          attributes: {
            class: "prose focus:outline-none dark:prose-dark dark:text-gray-300 text-base",
          },
        },
        content,
        onUpdate: onUpdate ? onUpdate : () => {},
        extensions: [
          StarterKit,
          Placeholder.configure({
            showOnlyWhenEditable: false,
            placeholder,
          }),
          TaskList.configure({
            HTMLAttributes: {
              class: "pl-0",
            },
          }),
          TaskItem.configure({
            HTMLAttributes: {
              class: "before:hidden pl-0 flex items-center dumb-prose-remove",
            },
          }),
          Extension.create({
            // Do not insert line break when pressing CMD+Enter
            // Most of the time handled by upper components
            addKeyboardShortcuts() {
              return {
                "Cmd-Enter"() {
                  return true
                },
                "Ctrl-Enter"() {
                  return true
                },
              }
            },
          }),
          Link,
        ],
      },
      [onUpdate, content]
    )

    return (
      <div>
        <EditorContent editor={editor} className={className} />
      </div>
    )
  }
)
  • I added the onUpdate callback and the content prop to the dependency list, this causes the component to flicker (I assume because it is destroying itself every time the dependency changes)
  • I had to comment out the BubbleMenu because that also crashes after the internal gets destroyed

All in all, these seem major issues with tiptap, never thought I would face so many problems by trying to attach a simple onUpdate handler

ospfranco avatar Jan 18 '22 19:01 ospfranco

I got it working by using refs, which is very inelegant and makes the code quite hard to understand:

const [updateProjectMutation] = useMutation(updateProjectNotes)
const tiptapOnUpdateRef = useRef(async ({ editor }: EditorEvents["update"]) => {
  await updateProjectMutation({
    id,
    notes: JSON.stringify(editor.getJSON()),
  })
  refetch()
})

useEffect(() => {
  if (project) {
    const editor: Editor = tiptapRef.current?.getEditorInstance()
    if (editor && project.notes) {
      editor.commands.clearContent() // Needed because the checkboxes do not update
      editor.commands.setContent(JSON.parse(project.notes))
      tiptapOnUpdateRef.current = async ({ editor }) => {
        await updateProjectMutation({
          id,
          notes: JSON.stringify(editor.getJSON()),
        })
        refetch()
      }
    }
  }
}, [id, project])

ospfranco avatar Jan 19 '22 07:01 ospfranco

I just ran into the same issue, it caught me completely off guard. @ospfranco I've found this seems to work too. Not sure if it's the right / best way to use the off / on methods. 🤷

const { onChange } = props;

...

// Don't set onUpdate in here
const editor = useEditor(...);

...

useEffect(() => {
    editor.off("update");
    editor.on("update", ({ editor: updatedEditor }) => onChange(updatedEditor.getHTML()));
}, [editor, onChange]);

colindb avatar Jan 20 '22 19:01 colindb

@colindb I assumed setOptions should do something similar, but it doesn't seem to be working, in any case, both solutions attempt to do the same, I like yours better, since passing refs is somewhat verbose and hard to reason about

ospfranco avatar Jan 20 '22 19:01 ospfranco

I am not sure I understand the problem correctly. Can you set up an absolutely minimal codesandbox for this?

philippkuehn avatar Jan 26 '22 10:01 philippkuehn

yarn cache clean and node_modules + yarn.lock delete and yarn install. this may solve the problem.

productdevbook avatar Feb 08 '22 11:02 productdevbook

Hmm if you mean if callback should get updated, this has nothing to do with the node_modules, I already tried updating the latest version, but it is rather a problem of how the hook + editor instance works

The hook takes an array of dependencies to re-render the editor, in order for the callbacks to be updated one has to declare them in the dependency array of the hook, but this causes other issues, basically, the internal instance of the editor gets destroyed and re-created, this causes flickering issues and what not

The workarounds we found, are basically hard replacing the saved callbacks on the first instantiation of the hook, but it's not ideal because: a) it's not documented (I think) and b) it's just really awkward and hard to understand

ospfranco avatar Feb 08 '22 12:02 ospfranco

Hi @philippkuehn I have the similar issue. Here is a minimal codesandbox

Steps to reproduce:

  1. Type smth in editor, the message "updated" is logged as expected
  2. Click resubscribe
  3. Type smth in editor, the message "updated" is logged but the message "resubscribe updated" is expected.

Looking into core Editor code I see that subscription only happen in constructor https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/Editor.ts#L89 and if setOptions is used with new handlers, the event emitter contains "stale" handlers because there is no resubscription in setOptions.

roman-kulakov avatar Feb 23 '22 16:02 roman-kulakov

I have a similar problem where I created an extension that takes a function in (onSubmit) that gets called when a user presses MOD+Enter. However, the closure is stale.

amorriscode avatar Mar 09 '22 00:03 amorriscode

Hey everyone 👋,

I just want to share how we are handling this ourselves, and this is something we did from the beginning and never noticed any issues:

  • We have a wrapping component (i.e. RichTextEditor) around useEditor and <EditorContent> (just like the OP example)
  • We follow rules of hooks strictly, and every dependency of useEditor is added to the dependency array
  • We use a custom useEditor hook to make sure that the value returned is never null (this is not SSR compatible):
    • https://gist.github.com/ryanto/4a431d822a98770c4ca7905d9b7b07da
  • Then there are a bunch of things that we do in the consumer side:
    • We set the initial content like this:
      const contentRef = useRef(initialContent)
      return <RichTextEditor content={contentRef.current} />
      
      • This makes sure the editor instance is not recreated/rerendered when the content changes (i.e. when a user types in the editor)
    • We make sure to use useMemo where appropriate, for instance, our RichTextEditor component takes an extensions array to feed into useEditor, and since extensions is on the dependencies array, we need to memoize this value (rules of hooks)
    • We use useEventCallback instead of useCallback for every callback (e.g., onCreate, onUpdate, onSelectionUpdate, onTransaction, etc.)
  • We also use useImperativeHandle to expose the internal editor instance to parent components

And that's pretty much it... We don't seem to observe any flickers, every callback seems to work as expected, and everything is updated when we need it.

rfgamaral avatar Mar 09 '22 09:03 rfgamaral

I faced the same issue and created a codesandbox for it. To test it look at the console and try to select text in the editor.

Expected behavior: be able to continue selecting while onSelectionUpdate changes.

This seems to be a working workaround for now:

editor.off("selectionUpdate")
editor.on("selectionUpdate", () => ...)

edit: I tested out .setOptions({...}) but it doesn't seem to update the listeners at all, not sure if I should file another bug for that?

https://codesandbox.io/s/gifted-river-1v58kf?file=/src/App.tsx

sharno avatar May 21 '22 00:05 sharno

Your missing the dependencies array in the useEditor hook. Add at least onChange in there, and it should work as you expect it.

Don't forget to wrap onChange with useCallback on the consumer, otherwise the editor will be recreated with every keystroke.

rfgamaral avatar May 21 '22 09:05 rfgamaral

I didn't get what you mean, I'm using [state] as a dependency array in useEditor and it's causing it to rerender the whole editor while the state changes every second, so if you try to select the text you cannot really select it.

I didn't understand where the onChange you mean is? Can you show your edits in a codesandbox?

sharno avatar May 22 '22 02:05 sharno

This was very hard to find. Please add this to the quickStart docs?

gthemiller avatar Jun 11 '22 08:06 gthemiller

Any updates on this issue? I have the same problem. I cannot use closures inside Tiptap extensions. They are always stale

dbousamra avatar Jul 12 '22 02:07 dbousamra

  • We follow rules of hooks strictly, and every dependency of useEditor is added to the dependency array ---> OF useEditor

For whatever reason, I completely did NOT understand the first 5 times I read this. The answer makes me feel stupid for wasting days. I completely neglected to realize this is a hook, just like useEffect. Use the dependency array.

const editor = useEditor(
    {
      //all the things
    },
    [valuesEditorNeedsToTrack, valuesEditorNeedsToTrack]
);

jedgrant avatar Jul 22 '22 02:07 jedgrant

@rfgamaral - would you mind providing a small amount of sample code to show you you can use an external function inside of the onUpdate call? Specifically elaborating on this?

useEventCallback instead of useCallback for every callback (e.g., onCreate, onUpdate, onSelectionUpdate, onTransaction, etc.)

When call the effect useEditor, I define the code I want to call in that callback declaration. But when I call other functions inside of onUpdate, those functions have stale data, which I assume means the function itself is stale, but I don't know. useEventCallback and useCallback but have no familiarity with how to use them in this context to force the functions to be able to use fresh data.

My editor looks like this

const editor = useEditor(
    {
      extensions: [StarterKit, Comment, Underline],
      //autofocus: "start", //autofocus: getPosition() || "start",
      content: contentRef.current,
      onUpdate({ editor }) {
        if (commentAction) return;
        clearTimeout(timerRef.current);
        timerRef.current = setTimeout(() => {
          handleChange(editor.getHTML(), documentPosition, projectData); //Stale closures
        }, 2500);
      },
    },
  ), [timerRef, chapterIdRef.current];

onUpdate always sends stale data in the form of projectData to handleChange If I add handleChange to the dependency array of the editor, it gets the right data, but then it rerenders the editor causing flickering and losing focus and cursor position.

Also, handleChange is in the parent component, passed as a prop to the editor component, and handle change also is in the form of (which I think does nothing because the editor is sending old data because it doesn't realize there's a newer version of the function?

const updatedData = useCallback(await handleContentChange(projectData) {

  },[projectData]);

Moving the onUpdate part out entirely like this may actually be working with focus remaining and no flickering...

useEffect(() => {
    const onChange = (editor) => {
      if (commentAction) return;
      clearTimeout(timerRef.current);
      console.log("%cClear", "color: cyan");
      timerRef.current = setTimeout(() => {
        console.log("%cExecuting", "color: lime");
      }, 2500);
    };
    editor &&
      editor.on("update", ({ editor }) =>
        onChange(editor)
      );
    return () => {
      editor && editor.off("update", ({ editor }) => onChange(editor));
    };
  }, [documentPosition, editor, handleChange, projectData]);

Last one hopefully, I think I finally understood the gentlemen's comment from above.

This is the onChange function for me, something I made, and used the useCallback hook with it so that it doesn't create MANY instances of the function and cause each press to call it dozens of times.

const onChange = useCallback(({editor}) => {
    if (commentAction) return;
    clearTimeout(timerRef.current);
    console.log("%cClear", "color: cyan");
    timerRef.current = setTimeout(() => {
      console.log("%cExecuting", "color: lime");
      //handleUpdateChapter({ ...chapter, content: editor.getHTML() }); //WORKS RIGHT
      handleChange(editor.getHTML(), documentPosition, projectData); //Stale closures
    }, 2500);
  }, [documentPosition, handleChange, projectData]);

onChange is in the dependency array of the useEffect that is keeping track of the editor so that the editor, and the onChange function are always in sync. At least I hope so 😅

  useEffect(() => {
    editor && editor.on("update", onChange);
    return () => {
      editor && editor.off("update", onChange);
    };
  }, [editor, onChange]);

jedgrant avatar Jul 24 '22 01:07 jedgrant

So I had the same issue as everyone here, this is how I fixed it in the end: image

So small clarifications.. the importatant thing is this:

  const editor = useEditor(
   {
     extensions: [StarterKit, Document, Paragraph, Text, Image, Dropcursor],
     content: editorContent,
     // onUpdate: ({ editor }) => {
     //   setEditorContent(editor.getHTML());
     // },
     onBlur: ({ editor }) => {
       setEditorContent(editor.getHTML());
     },
   },
   [editorContent]
 );

I've added a "dependency" to the state of the content of my Editor. So everytime I do "onBlur" the function will be called.

Also I've added a useEffect which just updates the state value when the data loads. The tricky part was understanding that the "Dependency" in this case editorContent actually triggers after onBlur in this case. At least thats how I understood it, please correct me if I'm wrong.

When I used onUpdate I always lost focus on the Editor and had to write letter by letter and click on the input everytime, but with "onBlur" that is not the case anymore

chrome_u4oKIgKGW7

alminisl avatar Sep 15 '22 12:09 alminisl

Any update on this? It seems to still be the case as of today.

piotrkulpinski avatar Oct 16 '22 21:10 piotrkulpinski

Here is a demo of how I got callbacks working:

https://codesandbox.io/s/upbeat-hermann-d3dlw5?file=/src/App.js:1634-1670

joe-pomelo avatar Oct 18 '22 21:10 joe-pomelo

Here is a demo of how I got callbacks working:

codesandbox.io/s/upbeat-hermann-d3dlw5?file=/src/App.js:1634-1670

@joe-pomelo Thanks! That's what I did as well. Would be great if I could use the default onUpdate method for this tho.

piotrkulpinski avatar Oct 19 '22 07:10 piotrkulpinski

I'm also struggling with this. I have a setup similar to the ones above, with a wrapper passing dynamic content down to the RichTextEditor and handling updates. The problem is when content changes, I call the onChange through the onUpdate, it updates Firestore, comes back and causes the editor to lose focus, so I can't continue typing.

I could call onChange through the onBlur but I don't want users to lose data if they type and close the tab without blurring. How is everyone handling that flow of content being updated and coming back?

Here is my code for reference:

function RichEditor(props) {
  const { hideToolbar, content, onChange } = props;

  let editor = useEditor(
    {
      extensions: [
        StarterKit,
        TaskList.configure({
          HTMLAttributes: {
            class: 'task-list',
          },
        }),
        TaskItem.configure({
          nested: true,
        }),
      ],
      parseOptions: {
        preserveWhitespace: true,
      },
      content: content,

      onUpdate({ editor }) {
        onChange(editor);
      },
    },
    [content]
  );

  return (
    <>
      <EditorContent editor={editor} className='note' />
      {!hideToolbar && <MenuBar editor={editor} />}
    </>
  );
}

And the RichEditor instance:

<RichEditor
      hideToolbar={hideToolbar}
      content={note.body_html}
      onChange={updateNoteDebounced}
 />

updateNoteDebounced will send the note to Firestore, which will then re-render the component above and send the new content (note.body_html) back to the editor, causing the lose of focus.

matheusbaumgart avatar Nov 25 '22 07:11 matheusbaumgart

I'm struggling so much with this 😭

matheusbaumgart avatar Dec 22 '22 07:12 matheusbaumgart

I've had the same issue with a stale onUpdate function, but I managed to fix it with this:

export default function TipTap({ onChange }) {
  // Save the content in a local state
  const [theContent, setTheContent] = useState<string>();

  const editor = useEditor(
    {
      // ...
      onUpdate: ({ editor }) => {
        // Update the local state with the content of the editor
        setTheContent(editor.getHTML());
      },
      // ...
    },
  );

  // Listen for changes to the theContent and call the 'onChange' prop
  useEffect(() => {
    if (!(theContent && props.onChange)) {
      return;
    }

    props.onChange(theContent);
  }, [theContent]);

  return <EditorContent editor={editor} />;
}

So far it seems to work as I want it to :)

jezzdk avatar Feb 02 '23 11:02 jezzdk

[content]

Did this work for you? Passing in [content] as dependency of useEditor is not doing anything for me. The problem is that I have multiple notes to edit with the TipTapEditor and the contents of the editor should dynamically change depending on what note I select.

So I need to 1. pass note as the content to update contents for every time I change note and 2. update note when I type in the editor. But it looks like this two functions are stepping in each others toes because when I type something on the editor it runs onUpdate which re-renders the editor because note is passed as a dependency.

My code:

const TipTapEditor = ({ note, setNote }) => {

    const editor = useEditor(
        {
            extensions: [StarterKit, Underline],
            content: note,
            onUpdate({ editor }) {
                setNote(editor.getHTML());
            },
        },
        [note]
    );

    return (
        <NotesBorder className="text-editor">
            <MenuBar editor={editor} />
            <EditorContent editor={editor} />
        </NotesBorder>
    );
};

export default TipTapEditor;

RipaltaOriol avatar Feb 04 '23 18:02 RipaltaOriol

I had exactly the same problem with the TipTap editor. I wanted to create a generic TextEditor component, which I in turn use from several other components. From these other components I wanted to always be able to retrieve the state of the TextEditor component (e.g. to provide adhoc validation), but as has already been written here, the updates within the editor instance were not correctly propagated out to the parent.

I have now found another solution for this. In doing so, I still have a generic TextEditor component, but I exported a method outside the rendering function that returns the useEditor hook and at the same time uses the extensions and options I need. I then have to use this extracted method in my other components and can then use my generic TextEditor component at the same time and have the ability to check the current state at any time.

TextEditor.tsx:

import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Placeholder from "@tiptap/extension-placeholder";
import Text from "@tiptap/extension-text";
import { Editor, EditorContent, useEditor } from "@tiptap/react";
import { styled } from "@stitches/react";

const TextEditor = styled("div", {
  ".text-editor": {
    "> p": {
      margin: 0,

      "&.is-editor-empty:first-child::before": {
        color: "grey",
        content: "attr(data-placeholder)",
        float: "left",
        height: 0,
        pointerEvents: "none",
      },
    },
    "&[contenteditable=false] > p.is-editor-empty": {
      cursor: "pointer",
    },
    minHeight: "200px"

  },
});

export const useTextEditor = (
  isInitiallyEditable = true,
  content?: string
): Editor | null => {
  return useEditor({
    extensions: [
      Document,
      Text,
      Paragraph,
      Placeholder.configure({
        placeholder: "Type to add something …",
        showOnlyWhenEditable: false,
      })
    ],
    editorProps: {
      attributes: {
        class: "text-editor",
      },
    },
    content: content ? JSON.parse(content) : null,
    editable: isInitiallyEditable,
    autofocus: true,
  });
};

interface TextEditorProps {
  editor: Editor | null;
}

const DefaultTextEditor: React.FC<TextEditorProps> = ({ editor }) => {
  return (
    <TextEditor>
      <EditorContent editor={editor} />
    </TextEditor>
  );
};

export default DefaultTextEditor;

EditorContainer.tsx:

import DefaultTextEditor, { useTextEditor } from "./TextEditor";

const EditorContainer: React.FC = () => {
  const editor = useTextEditor();
  return (
    <>
      <div>is editor empty?: {editor?.isEmpty ? "true" : "false"}</div>
      <span>content: {JSON.stringify(editor?.getJSON())}</span>
      <DefaultTextEditor editor={editor} />
    </>
  );
};

export default EditorContainer;

here is my working codesandbox: https://codesandbox.io/s/tiptap-editor-state-between-child-and-parent-component-9m0bkh

mclazer avatar Feb 05 '23 19:02 mclazer

@mclazer I really like your approach! I've tried replicating it and although I had some success for the most part I'm finding it difficult to update the contents of the editor; I want to change the text editor when necessary.

For example, I initialize useTextEditor to text in row 0, but when I click on row 5 I want to be able to update the contents of the editor to the text in there. I've tried using custom handler functions and useEffect but so far nothing is working because useTextEditor is a hook itself. Were you able to find a work around?

RipaltaOriol avatar Feb 18 '23 13:02 RipaltaOriol

@RipaltaOriol so you have different components for each row and the initialization of the editor is in row 0? Or do you just have 1 component and different rows and set the text initially to row0 and want to be able to click other rows and update the text accordingly. Could you provide a codesandbox with the code? Do you mean something like this here?: https://codesandbox.io/s/tiptap-editor-update-text-from-rows-60lci8?file=/src/EditorContainer.tsx

mclazer avatar Feb 18 '23 17:02 mclazer

@mclazer its the latter. I solved the problem thanks to you sandbox. I was importing the useTextEditor wrong and caused React to think the function was a custom hook instead.

RipaltaOriol avatar Feb 19 '23 22:02 RipaltaOriol