Fluid Framework: Undo/redo and transactions in SharedTree

Nick Simons

Learn more about SharedTree and how it transforms how you use Fluid Framework here: https://aka.ms/fluid/tree

How SharedTree supports undo/redo

One of the challenges of collaborative editing is how to handle undo/redo operations. If a user wants to undo their own changes, that operation should work in the context of any remote changes that may have occurred in the meantime. And, as much as possible, undo should return the impacted region of the document to the state the user saw before making their change. Similarly, if a user wants to redo their changes, the redo should reapply and consider any remote changes.

If you want to undo a change, you apply the reverse of the change. SharedTree can do this well because SharedTree’s edits work to capture precise intentions. This means that even in case of a remote change, the undo operation will still result in a rational and deterministic outcome. (Learn more about how SharedTree merges changes here: https://aka.ms/fluid/tree/merge.)

Revertible objects

For each change to the tree, you can get a Revertible object that allows you to revert that change. So, to include a change to the tree in your undo stack, put the Revertible object for that change on your undo stack. Note that there is a memory cost associated with Revertible objects as the client must track changes from that point in case it needs to revert. You can choose how best to manage this as you can dispose of Revertible objects whenever you see fit.

If a user wants to undo a change, you simply call the revert() method on the appropriate Revertible object. By default, this will dispose of the Revertible object. Also, you can get a Revertible object for the undo operation and put that on your redo stack.

To get Revertible objects, you must listen for “commitApplied” events on the TreeView – the object you use to interact with a SharedTree. Each “commitApplied” event includes a RevertibleFactory that you can use to get the Revertible object. Requiring that you explicitly get the Revertible object ensures that you don’t unintentionally pay the memory cost.

Here is a simple example that puts Revertible objects on the undo and redo stacks based on the kind of commit (default, undo, or redo).

Here is an example that pops the undo or redo stack and reverts a change.

Transactions and undo

Frequently there isn’t a one-to-one match between application-level activities such as removing an item and the underlying changes to the tree. When you want to express a set of tree changes as a single action, you can wrap that set of changes in a transaction. This ensures that the changes are treated atomically.

To create a transaction, you use the runTransaction() method. Place any tree operations you want to wrap in the transaction in a function and pass that function into the runTransaction() method. For example, this method moves all the items from a group (this in the example) before removing it:

By using a transaction in this case, if the user chooses to undo this operation, the group will be restored AND all the items will be moved back into the group.

Follow us on X (Twitter) / @Microsoft365Dev and subscribe to our YouTube channel to stay up to date on the latest developer news and announcements.


Leave a comment

Feedback usabilla icon