question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

onUpdate callback does not update after re-render

See original GitHub issue

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?

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

Are you sponsoring us?

  • Yes, I’m a sponsor. 💖

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:9
  • Comments:24 (3 by maintainers)

github_iconTop GitHub Comments

17reactions
colindbcommented, Jan 20, 2022

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]);
10reactions
gthemillercommented, Jun 11, 2022

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why is useState not triggering re-render? - Stack Overflow
To anyone confused as to why this happens, is because setSomething() only re renders the component if and only if the previous and...
Read more >
When does React re-render components? - Felix Gerschau
In React hooks, the forceUpdate function isn't available. You can force an update without altering the components state with React.useState like ...
Read more >
How to stop re-rendering lists in React? - Alex Sidorenko
Components always re-render ... First, let's simplify our example by removing all props from the Item . We will still update the parent...
Read more >
Using the Effect Hook - React
Whether or not you're used to calling these operations “side effects” (or ... Sometimes, we want to run some additional code after React...
Read more >
Re-render & Optimization in React | by Saima Rahman - Medium
In react, the only way a component will update is when there is a ... We will move the call-back function from the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found