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

Fix React Dependency Array creating new editor instances instead of reusing existing one #4453

Closed
wants to merge 4 commits into from

Conversation

bdbch
Copy link
Contributor

@bdbch bdbch commented Sep 15, 2023

Please describe your changes

Before this PR everytime dependencies change the whole editor gets replaced with a new instance. This would remove entered content, cause flashes and gets rid of the editor state (which sucks for example because of the history living in the state for example)

This PR implements a check that will update the editor options when the deps change instead of creating a new instance with said options.

How did you accomplish your changes

Added a check in the react useEditor hook to update the editor instead of replacing it.

How have you tested your changes

I tested my changes on the demo page I created (Link: https://deploy-preview-4453--tiptap-embed.netlify.app/src/issues/2403/react/)

How can we verify your changes

See above

  1. Write something - in the console you should see 0
  2. Click on Inc a few times
  3. Write something - now the onUpdate function inside the editor should be updated
  4. Check the editor class (it should be my-editor
  5. Click on the Update class button
  6. The editor class should now correctly be my-editor updated-editor

Remarks

On my tests everything worked fine for now but I think we'd need to verify that this change is not breaking some special cases.

Checklist

  • The changes are not breaking the editor
  • Added tests where possible
  • Followed the guidelines
  • Fixed linting issues

Related issues

This commit adds a check to not create a new editor instance whenever the deps change but update the editorOptions object via editor.setOptions
@bdbch bdbch self-assigned this Sep 15, 2023
@netlify
Copy link

netlify bot commented Sep 15, 2023

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit 6f6e49e
🔍 Latest deploy log https://app.netlify.com/sites/tiptap-embed/deploys/661084773d4e320007f645d3
😎 Deploy Preview https://deploy-preview-4453--tiptap-embed.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@bdbch bdbch changed the base branch from develop to main September 15, 2023 15:03
@andrictham
Copy link

andrictham commented Oct 13, 2023

@bdbch Friendly bump! Noticed that this PR has gone stale.

We’re running into this exact issue in our app, where if React rerenders, it causes the editor to unmount and then remount.

Hoping to see this merged soon.

@bdbch
Copy link
Contributor Author

bdbch commented Oct 18, 2023

@svenadlung could you take a look?

svenadlung
svenadlung previously approved these changes Oct 19, 2023

return () => {
isMounted = false
}
}, deps)
}, [deps])
Copy link
Contributor

Choose a reason for hiding this comment

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

This line wrongly double-wraps the dependencies in an additional array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks - fixed

@Nantris
Copy link
Contributor

Nantris commented Nov 8, 2023

I noticed an issue that I commented on.

In theory this seems like exactly what we need to avoid the insanity of passing a zustand store to our Extensions just to give them up-to-date functions to read from, or otherwise passing down all of their relevant arguments. In practice it doesn't seem to work that way though.

But even after implementing this with the line I commented on fixed, it seems like the option passed to an Extension are not updated. Eg we are trying to get data from an external store but the function is still using the outdated version of the store. I'd expect that editor.setOptions(options) would make the extensions use the latest versions of their options. Is that not intended to be the case?

Specifically I'd expect addProseMirrorPlugins to run anew, but I guess that may not be the intention since I imagine that would break plugin states. @bdbch can you clarify how this should work?

Also, just for reference, this will break any code that relied on the editor re-rendering on every update of the editor, eg for a ControlledBubbleMenu or the like, (although that should be trivial for users to fix.)

I hope this will get merged into 2.2.0-rc.5 soon!

@andrictham
Copy link

andrictham commented Nov 14, 2023

@slapbox

exactly what we need to avoid the insanity of passing a zustand store to our Extensions just to give them up-to-date functions to read from, or otherwise passing down all of their relevant arguments

This is close to the approach I’m taking right now for the Mention suggestions list to be reactive, except I used Legend State.

I’m also unsure if this PR being merged will mean I no longer have to do that.

I’m not familiar with TipTap’s internals and how all their extensions are authored. But in my specific case, I was interested in using the Mention extension and wanted the suggestions list to be reactive.

Reading their code for the Mention and Suggestion extensions, it seems that, potentially, the issue is caused by suggestions being passed into Prosemirror as a prop.

I’m not schooled enough in Prosemirror internals to know if this is exactly the issue, but I gathered from a Prosemirror forum thread that, unlike React, Prosemirror views don’t update when props change, only when state changes. So, if you pass in something that you expect to change, you lose reactivity once you pass it down.

If this is indeed the case, then it means TipTap needs to do more work to ensure props being passed to Prosemirror work more like props in React, so that Prosemirror internals (which is supposed to be abstracted away from us) don’t leak out into our app.

The suggested fix was to pass an observable object down. This fix worked in my case, using Legend State to create an observable for the list of suggestions and then passing it over to TipTap’s Mention extension.

Using Legend State, I was able to preserve reactivity as data is passed from React to Prosemirror and back to React again (since the Suggestion extension renders a React component), which is nice.

The issue of TipTap not playing nice with regular React state/props, however, remains. What I expect is for the editor not to destroy itself and be recreated just for props to update. I expect TipTap to hook properly into Prosemirror so that props passed into it are reactive in a way one would expect any React component to be.

@Palanikannan1437
Copy link

@bdbch @svenadlung would love to see this get merged 🙌

@bdbch
Copy link
Contributor Author

bdbch commented Apr 5, 2024

But even after implementing this with the line I commented on fixed, it seems like the option passed to an Extension are not updated. Eg we are trying to get data from an external store but the function is still using the outdated version of the store. I'd expect that editor.setOptions(options) would make the extensions use the latest versions of their options. Is that not intended to be the case?

Right now setOptions only updates the editors state & editor props but does not run any extension setup again, see here:

if (this.options.editorProps) {
this.view.setProps(this.options.editorProps)
}
this.view.updateState(this.state)
}

Before this PR the editor instance actually gets completely replaced which would mean that the extension manager will also run through another time.

Could you maybe createa Codesandbox / Stackblitz showcasing what you want to do with Prosemirror plugins accessing external data, I could try to use that to work off from.

Specifically I'd expect addProseMirrorPlugins to run anew, but I guess that may not be the intention since I imagine that would break plugin states. @bdbch can you clarify how this should work?

I think that would be a case yes - there's no easy way to persist a plugin state.

Also, just for reference, this will break any code that relied on the editor re-rendering on every update of the editor, eg for a ControlledBubbleMenu or the like, (although that should be trivial for users to fix.)

That's true - @svenadlung what your thought on that? That would mean we need to push the version up to 3.0.0 for the next release with this PR merged.


I’m not familiar with TipTap’s internals and how all their extensions are authored. But in my specific case, I was interested in using the Mention extension and wanted the suggestions list to be reactive.

This PR won't fix that as the mention plugin won't rerun (see above). But when it comes to updating Prosemirror props & state the setOptions function does that:

if (this.options.editorProps) {
this.view.setProps(this.options.editorProps)
}
this.view.updateState(this.state)
}


In general I think there are a few misconceptions right now when it comes to reactivity and the useEditor hook - while the hook will run whenever you update the dependencies - the editor won't just take all reactive props and update them down to all plugins and extensions registered (as for now).

Feel free to send in Sandboxes which we can use to work from as references or maybe even open a PR for that issue if you find a good fix for this - I'd be down to play around with those reactivity problems.

Back to the suggestion issue: What I did was completely doing data fetching / reactivity inside my React SuggestionList component rendered inside my suggestion popup which was reacting to query changes. That means I didn't use the items array at all. (Not the cleanest solution but worked).

@nperez0111
Copy link
Contributor

Resolved with: #5161

@nperez0111 nperez0111 closed this Jul 11, 2024
@nperez0111 nperez0111 deleted the bdbch/fix-react-hook-deps branch August 14, 2024 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

onUpdate callback does not update after re-render
6 participants