diff --git a/src/structs/Item.js b/src/structs/Item.js
index 7ac80f5d..0b39a246 100644
--- a/src/structs/Item.js
+++ b/src/structs/Item.js
@@ -127,12 +127,13 @@ export const splitItem = (transaction, leftItem, diff) => {
  * @param {Item} item
  * @param {Set<Item>} redoitems
  * @param {DeleteSet} itemsToDelete
+ * @param {boolean} ignoreRemoteMapChanges
  *
  * @return {Item|null}
  *
  * @private
  */
-export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
+export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
   const doc = transaction.doc
   const store = doc.store
   const ownClientID = doc.clientID
@@ -152,7 +153,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
   // make sure that parent is redone
   if (parentItem !== null && parentItem.deleted === true) {
     // try to undo parent if it will be undone anyway
-    if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null)) {
+    if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges) === null)) {
       return null
     }
     while (parentItem.redone !== null) {
@@ -198,7 +199,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
     }
   } else {
     right = null
-    if (item.right) {
+    if (item.right && !ignoreRemoteMapChanges) {
       left = item
       // Iterate right while right is in itemsToDelete
       // If it is intended to delete right while item is redone, we can expect that item should replace right.
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js
index c2a4dcc8..21892ff2 100644
--- a/src/utils/UndoManager.js
+++ b/src/utils/UndoManager.js
@@ -101,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
         }
       })
       itemsToRedo.forEach(struct => {
-        performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions) !== null || performedChange
+        performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
       })
       // We want to delete in reverse order so that children are deleted before
       // parents, so we have more information available when items are filtered.
@@ -137,6 +137,7 @@ const popStackItem = (undoManager, stack, eventType) => {
  * filter returns false, the type/item won't be deleted even it is in the
  * undo/redo scope.
  * @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
+ * @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
  */
 
 /**
@@ -153,7 +154,7 @@ export class UndoManager extends Observable {
    * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
    * @param {UndoManagerOptions} options
    */
-  constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
+  constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false } = {}) {
     super()
     /**
      * @type {Array<AbstractType<any>>}
@@ -180,6 +181,7 @@ export class UndoManager extends Observable {
     this.redoing = false
     this.doc = /** @type {Doc} */ (this.scope[0].doc)
     this.lastChange = 0
+    this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
     /**
      * @param {Transaction} transaction
      */
diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js
index 3ce65d5e..c1889d9f 100644
--- a/tests/undo-redo.tests.js
+++ b/tests/undo-redo.tests.js
@@ -565,3 +565,26 @@ export const testUndoDeleteTextFormat = tc => {
   t.compare(text.toDelta(), expect)
   t.compare(text2.toDelta(), expect)
 }
+
+/**
+ * Undo text formatting delete should not corrupt peer state.
+ *
+ * @see https://github.com/yjs/yjs/issues/392
+ * @param {t.TestCase} tc
+ */
+export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
+  const doc = new Y.Doc()
+  const doc2 = new Y.Doc()
+  doc.on('update', update => Y.applyUpdate(doc2, update, doc))
+  doc2.on('update', update => Y.applyUpdate(doc, update, doc2))
+  const map1 = doc.getMap()
+  const map2 = doc2.getMap()
+  const um1 = new Y.UndoManager(map1, { ignoreRemoteMapChanges: true })
+  map1.set('x', 1)
+  map2.set('x', 2)
+  map1.set('x', 3)
+  map2.set('x', 4)
+  um1.undo()
+  t.assert(map1.get('x') === 2)
+  t.assert(map2.get('x') === 2)
+}