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

[Bug]: onUpdate fires when no content update has occurred #4535

Open
1 of 2 tasks
Nantris opened this issue Oct 15, 2023 · 14 comments
Open
1 of 2 tasks

[Bug]: onUpdate fires when no content update has occurred #4535

Nantris opened this issue Oct 15, 2023 · 14 comments
Assignees
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug

Comments

@Nantris
Copy link
Contributor

Nantris commented Oct 15, 2023

Which packages did you experience the bug in?

core

What Tiptap version are you using?

2.1.12

What’s the bug you are facing?

This makes listening to the editor for updates extremely tedious and expensive in terms of both memory and CPU since we need to store an external reference and then compare against it.

What browser are you using?

Chrome

Code example

No response

What did you expect to happen?

onUpdate should not fire at editor creation time, but it does.

Anything to add? (optional)

No response

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. 💖
@Nantris Nantris added Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug labels Oct 15, 2023
@bdbch
Copy link
Contributor

bdbch commented Oct 15, 2023

Could you add more infos?

  • Do you use collab?
  • When do these onUpdate events occur?
  • Was this introduced at 2.1.12 or did you experienced this earlier?
  • Are there any new extensions you are using which could cause those update calls?

https://codesandbox.io/s/rough-butterfly-k2t855?file=/src/App.js

This minimal setup doesn't seem to have any unnecessary onUpdate calls so I wonder if it's an extension that was introduced.

@Nantris
Copy link
Contributor Author

Nantris commented Oct 15, 2023

Thanks for the speedy reply and the codesandbox @bdbch. It's quite strange.

  • No collab
  • At startup only - when I'd expect only onCreate to run. onUpdate never runs errantly besides that
  • I only started wiring things up to an external data source with 2.1.12` so unfortunately I can't say.
  • I disabled all of the extensions besides StarterKit.

I also thought it might be due to StrictMode but apparently that's not the case. It's possible I made a mistake and didn't really disable everything that might interact with it but I disabled all extensions and only set a content and the on[Event] functions on the editor as far as I know during my test.

I'll have to loop back on this I guess. Thanks again for the speedy reply!

@Nantris
Copy link
Contributor Author

Nantris commented Oct 17, 2023

Indeed I cannot repro this today. I don't know what the issue was. I'll re-open if I discover actionable details. In the meantime I'm using this alternative:

onTransaction: ({ editor, transaction }) => {
        if (transaction.docChanged) onUpdate({ editor });
      },

@Nantris Nantris closed this as completed Oct 17, 2023
@Nantris
Copy link
Contributor Author

Nantris commented Oct 17, 2023

Whoops a flaw in my testing. I seem to have discovered the cause. Running editor.setEditable and setting the same value as the existing one provokes onUpdate to fire.

Here's a repro: https://codesandbox.io/s/zealous-kate-d6s224?file=/src/App.js

@Nantris Nantris reopened this Oct 17, 2023
@akucinskii
Copy link

I think that adding a second argument false to the editor.setEditable fixes your problem.

repro:
https://codesandbox.io/s/objective-fermi-wnyrj5?file=/src/App.js

@Nantris
Copy link
Contributor Author

Nantris commented Oct 26, 2023

Ah jeez I didn't notice that in the docs. Thanks @akucinskii

I really don't think that it makes sense to default that to true, but I'll close this issue I guess.

@Nantris Nantris closed this as completed Oct 26, 2023
@Nantris Nantris reopened this Feb 15, 2024
@Nantris
Copy link
Contributor Author

Nantris commented Feb 15, 2024

This can occur in other cases, but I'm not really clear on the nature of those.

It seems like if modifications are made to the document that don't actually affect the serializable content it still fires. I guess this is the intended behavior but the docs could use an update.

Maybe it just checks for transaction.docChanged as I used above? I couldn't find the relevant code for onUpdate when I tried.

@jpobley
Copy link
Contributor

jpobley commented Aug 5, 2024

onUpdate will also fire even if a transaction has been filtered by a plugin. Here's a minimal reproduction that filters every transaction but logs out calls to onUpdate:

https://codesandbox.io/p/sandbox/billowing-pond-2t5md7?workspaceId=a77e85f9-e56d-47f2-ae56-008fa38d6d0c

@nperez0111
Copy link
Contributor

nperez0111 commented Aug 6, 2024

So this is the function that handles the update events:

if (!transaction.docChanged || transaction.getMeta('preventUpdate')) {
return
}
this.emit('update', {
editor: this,
transaction,
})

I think we can make a relatively easy optimization here, if the editor.state is equal to the editor.state from before, that means none of the transactions could be applied and we can just skip calling the rest of the function.

But to be clear, this event is emitted only when the transaction does have changes or when setEditable is modified without the second parameter:

this.emit('update', { editor: this, transaction: this.state.tr })

For that reason, I don't think that the original issue is valid

I made a draft of this here: #5449

@jpobley
Copy link
Contributor

jpobley commented Aug 6, 2024

if the editor.state is equal to the editor.state from before, that means none of the transactions could be applied and we can just skip calling the rest of the function.

We don't even need to compare the states. If we use state.applyTransaction() instead of state.apply(), we can inspect the transactions field that's returned and determine if it includes the transaction that was passed in:

const {state, transactions} = this.state.applyTransaction(transaction)
const trWasApplied = transactions.includes(transaction)

if (!trWasApplied) {
    return
}

@nperez0111
Copy link
Contributor

@jpobley that is definitely useful to know!
Does beg the question of what to do with appended transactions, but will look into it more later

@bdbch
Copy link
Contributor

bdbch commented Aug 7, 2024

Would we need to keep track of all appended transactions and check for all of them to be included in the transactions array then?

@jpobley
Copy link
Contributor

jpobley commented Aug 7, 2024

It's probably good practice and more accurate to look at the appended transactions. However, they've been ignored up until now so maybe it doesn't matter and can wait until a major rev since the behavior of dispatchTransaction() will change.

My two cents:

  • Publish beforeTransactionevent
  • If the transactions field returned by state.applyTransaction() is empty, then the rootTr was filtered out and we can return there.
    • Maybe still publish the transaction event but with a flag that lets the caller know it was filtered out. Not sure if that's useful to the caller.
  • Otherwise, publish the transaction event.
  • selectionUpdate can continue as is
  • We'll then need to loop through the transactions array to determine of focus, and blur need to be published, since technically one transaction might undo a previous transaction's changes.
  • Finally, it's probably easier to compare states at this point in order to determine if update needs to be called rather than trying to figure that out while looping through the transactions array

There's a question about how to publish any appended transactions. We can add an appendedTransactions field alongside the current transaction field, which would simply be transactions.slice(1). There's an argument to be made for keeping the transactions together, but this way won't be a breaking change to the event data interface. 🤷

@nperez0111
Copy link
Contributor

Appreciate the thorough thoughts on this. We are actually planning on a tiptap v3 so if we want to make a breaking change I think it'd better fit there than mess with it in the maintenance version.

If you would like, feel free to make a PR with your suggestions otherwise I'll throw this in a backlog of tasks for V3 and we can get to it eventually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug
Projects
Status: Done
Development

No branches or pull requests

6 participants