lexical: Bug: async updates cause unwanted history

There’s a class of bugs involving async actions that leaves the history stack in an unwanted state. The problem occurs when a future update needs to replace a past update such as when uploading an image. Typically, an image node is inserted with a loading state because we don’t know the src until it’s uploaded. Eventually, we’ll set the src as the result of an async action. If the async update is not merged then it’s possible to “undo” back to the image’s initial loading state which the user would never want.

Updates that use discrete: true and the history-merge tag will properly squash the update, but only if there have been no other updates in between. There’s no way to merge a new update into a previous update of arbitrary position in the history stack.

The current way to solve this is to track data in a separate map keyed by node key. It’s cumbersome and feels like something that the library should provide a built-in solution for.

Lexical version: 0.9.0 (and earlier)

Steps To Reproduce

  1. Insert an image node with a loading indicator. Asynchronously upload the desired image to your backend of choice.
  2. Make random edits unrelated to the image
  3. When the async upload finishes, update the placeholder image node with the new src attribute.
  4. Trigger “undo” to reset the editor to state (2) where the image is still loading

Link to code example: https://codesandbox.io/s/lexical-plain-text-example-forked-usgq8e?file=/src/Editor.js

The current behavior

The user can “undo” back to an unwanted loading state.

The expected behavior

Previous history should be overwritten so that the user cannot return to unwanted loading states.

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 5
  • Comments: 17 (14 by maintainers)

Most upvoted comments

@fantactuka @thegreatercurve checking in again to see if y’all have any guidance on how to do the proposed history rewrite

Our current workaround is to keep this kind of data out of lexical as much as possible. For example, it feels natural to store >an image’s src on its node, but we have to keep it as React state since it’s modified once the upload completes. It’s >inconvenient and makes serialization more difficult.

I don’t think this is bad. One strategy we make use of internally that bypasses this problem completely is storing some sort of ID on the node, then using that to fetch or generate a URL at runtime and encapsulating the logic for doing that in the React component. @fantactuka is working on documenting this pattern.

I realize that it suggests a change in architecture, and I’d love to get you a solution that doesn’t force that, but I did want to call this one out. It’s not without it’s own complexities, though.

we allow uses to create their own history states with createEmptyHistoryState for these sort of edge cases

In my opinion, updating the editor as a result of an async action is not an edge case. It’s anecdotal, but I’ve seen several people with similar issues in the Lexical Discord.