diff --git a/aleksis/core/assets/components/LegacyBaseTemplate.vue b/aleksis/core/assets/components/LegacyBaseTemplate.vue
index ecb534fdbbad3de5883f42c5563a47376f63f22d..3f620e7a0b04e16eae53e15b505f4b847064c6b0 100644
--- a/aleksis/core/assets/components/LegacyBaseTemplate.vue
+++ b/aleksis/core/assets/components/LegacyBaseTemplate.vue
@@ -73,7 +73,7 @@ export default {
   },
   mounted() {
     // Subscribe to message channel to receive height from iframe
-    window.addEventListener("message", this.receiveMessage);
+    this.safeAddEventListener(window, "message", this.receiveMessage);
   },
   watch: {
     $route() {
@@ -81,10 +81,6 @@ export default {
       this.$root.contentLoading = true;
     },
   },
-  beforeDestroy() {
-    window.removeEventListener("message", this.receiveMessage);
-    this.$root.contentLoading = false;
-  },
   name: "LegacyBaseTemplate",
 };
 </script>
diff --git a/aleksis/core/assets/mixins/aleksis.js b/aleksis/core/assets/mixins/aleksis.js
new file mode 100644
index 0000000000000000000000000000000000000000..7dc82d67f9d687f0f5c218373b846685c6b2d307
--- /dev/null
+++ b/aleksis/core/assets/mixins/aleksis.js
@@ -0,0 +1,38 @@
+/**
+ * Mixin with utilities for AlekSIS view components.
+ */
+const aleksisMixin = {
+  data: () => {
+    return {
+      $_aleksis_safeTrackedEvents: new Array(),
+    };
+  },
+  methods: {
+    safeAddEventListener(target, event, handler) {
+      console.debug("Safely adding handler for %s on %o", event, target);
+      target.addEventListener(event, handler);
+      // Add to tracker so we can unregister the handler later
+      this.$data.$_aleksis_safeTrackedEvents.push({
+        target: target,
+        event: event,
+        handler: handler,
+      });
+    },
+  },
+  beforeDestroy() {
+    // Unregister all safely added event listeners as to not leak them
+    for (let trackedEvent in this.$data.$_aleksis_safeTrackedEvents) {
+      console.debug(
+        "Removing handler for %s on %o",
+        trackedEvent.event,
+        trackedEvent.target
+      );
+      trackedEvent.target.removeEventListener(
+        trackedEvent.event,
+        trackedEvent.handler
+      );
+    }
+  },
+};
+
+export default aleksisMixin;
diff --git a/aleksis/core/assets/mixins/offline.js b/aleksis/core/assets/mixins/offline.js
index dfdc034848483fdd93f4e42f3679821dc4727e12..6b9aaeba0f3bee2c6596a69d1a3011a87aa6b901 100644
--- a/aleksis/core/assets/mixins/offline.js
+++ b/aleksis/core/assets/mixins/offline.js
@@ -22,15 +22,15 @@ const offlineMixin = {
     };
   },
   mounted() {
-    window.addEventListener("online", () => {
+    this.safeAddEventListener(window, "online", () => {
       console.info("Navigator changed status to online.");
       this.checkOfflineState();
     });
-    window.addEventListener("offline", () => {
+    this.safeAddEventListener(window, "offline", () => {
       console.info("Navigator changed status to offline.");
       this.checkOfflineState();
     });
-    document.addEventListener("visibilitychange", () => {
+    this.safeAddEventListener(document, "visibilitychange", () => {
       console.info("Visibility changed status to", document.visibilityState);
       this.checkOfflineState();
     });
diff --git a/aleksis/core/assets/plugins/aleksis.js b/aleksis/core/assets/plugins/aleksis.js
index 9d511d823595b857ec66e60a9b0ebc1068004b28..d2c87d40d396da87beb308a60f9b6e9786c70dab 100644
--- a/aleksis/core/assets/plugins/aleksis.js
+++ b/aleksis/core/assets/plugins/aleksis.js
@@ -4,6 +4,7 @@
 
 // aleksisAppImporter is a virtual module defined in Vite config
 import { appMessages } from "aleksisAppImporter";
+import aleksisMixin from "../mixins/aleksis.js";
 
 console.debug("Defining AleksisVue plugin");
 const AleksisVue = {};
@@ -166,6 +167,9 @@ AleksisVue.install = function (Vue) {
       next();
     });
   };
+
+  // Add default behaviour for all components
+  Vue.mixin(aleksisMixin);
 };
 
 export default AleksisVue;