diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js
index b373e1d47..b4e400aeb 100644
--- a/resources/js/components/attachments.js
+++ b/resources/js/components/attachments.js
@@ -43,7 +43,9 @@ export class Attachments extends Component {
 
     reloadList() {
         this.stopEdit();
-        this.mainTabs.components.tabs.show('items');
+        /** @var {Tabs} */
+        const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
+        tabs.show('items');
         window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
             this.list.innerHTML = resp.data;
             window.$components.init(this.list);
diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js
index 241cdece9..205cbd8fd 100644
--- a/resources/js/components/code-editor.js
+++ b/resources/js/components/code-editor.js
@@ -126,7 +126,7 @@ export class CodeEditor extends Component {
         }
 
         this.loadHistory();
-        this.popup.components.popup.show(() => {
+        this.getPopup().show(() => {
             Code.updateLayout(this.editor);
             this.editor.focus();
         }, () => {
@@ -135,10 +135,17 @@ export class CodeEditor extends Component {
     }
 
     hide() {
-        this.popup.components.popup.hide();
+        this.getPopup().hide();
         this.addHistory();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popup, 'popup');
+    }
+
     async updateEditorMode(language) {
         const Code = await window.importVersioned('code');
         Code.setMode(this.editor, language, this.editor.getValue());
diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js
index 215c0b94e..572945d5a 100644
--- a/resources/js/components/confirm-dialog.js
+++ b/resources/js/components/confirm-dialog.js
@@ -34,7 +34,7 @@ export class ConfirmDialog extends Component {
      * @returns {Popup}
      */
     getPopup() {
-        return this.container.components.popup;
+        return window.$components.firstOnElement(this.container, 'popup');
     }
 
     /**
diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js
index 69534dea5..d455f7ee7 100644
--- a/resources/js/components/entity-selector-popup.js
+++ b/resources/js/components/entity-selector-popup.js
@@ -17,16 +17,26 @@ export class EntitySelectorPopup extends Component {
 
     show(callback) {
         this.callback = callback;
-        this.container.components.popup.show();
+        this.getPopup().show();
         this.getSelector().focusSearch();
     }
 
     hide() {
-        this.container.components.popup.hide();
+        this.getPopup().hide();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.container, 'popup');
+    }
+
+    /**
+     * @returns {EntitySelector}
+     */
     getSelector() {
-        return this.selectorEl.components['entity-selector'];
+        return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
     }
 
     onSelectButtonClick() {
diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js
index a78aa3483..a44fffc1b 100644
--- a/resources/js/components/image-manager.js
+++ b/resources/js/components/image-manager.js
@@ -94,7 +94,7 @@ export class ImageManager extends Component {
 
         this.callback = callback;
         this.type = type;
-        this.popupEl.components.popup.show();
+        this.getPopup().show();
         this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
 
         if (!this.hasData) {
@@ -104,7 +104,14 @@ export class ImageManager extends Component {
     }
 
     hide() {
-        this.popupEl.components.popup.hide();
+        this.getPopup().hide();
+    }
+
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popupEl, 'popup');
     }
 
     async loadGallery() {
diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js
index 41e070b9d..d6faabd05 100644
--- a/resources/js/components/page-editor.js
+++ b/resources/js/components/page-editor.js
@@ -196,7 +196,8 @@ export class PageEditor extends Component {
         event.preventDefault();
 
         const link = event.target.closest('a').href;
-        const dialog = this.switchDialogContainer.components['confirm-dialog'];
+        /** @var {ConfirmDialog} **/
+        const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
         const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
 
         if (saved && confirmed) {
diff --git a/resources/js/components/tag-manager.js b/resources/js/components/tag-manager.js
index b51cfe9b2..cfbc514a0 100644
--- a/resources/js/components/tag-manager.js
+++ b/resources/js/components/tag-manager.js
@@ -11,7 +11,8 @@ export class TagManager extends Component {
 
     setupListeners() {
         this.container.addEventListener('change', event => {
-            const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows'];
+            /** @var {AddRemoveRows} **/
+            const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
             if (!this.hasEmptyRows()) {
                 addRemoveComponent.add();
             }
diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js
index 549963eed..d4d88a633 100644
--- a/resources/js/components/user-select.js
+++ b/resources/js/components/user-select.js
@@ -4,12 +4,11 @@ import {Component} from "./component";
 export class UserSelect extends Component {
 
     setup() {
+        this.container = this.$el;
         this.input = this.$refs.input;
         this.userInfoContainer = this.$refs.userInfo;
 
-        this.hide = this.$el.components.dropdown.hide;
-
-        onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+        onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
     }
 
     selectUser(event, userEl) {
@@ -20,4 +19,10 @@ export class UserSelect extends Component {
         this.hide();
     }
 
+    hide() {
+        /** @var {Dropdown} **/
+        const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
+        dropdown.hide();
+    }
+
 }
\ No newline at end of file
diff --git a/resources/js/services/components.js b/resources/js/services/components.js
index 04fd0dcf4..c0fc12524 100644
--- a/resources/js/services/components.js
+++ b/resources/js/services/components.js
@@ -1,5 +1,21 @@
+/**
+ * A mapping of active components keyed by name, with values being arrays of component
+ * instances since there can be multiple components of the same type.
+ * @type {Object<String, Component[]>}
+ */
 const components = {};
-const componentMap = {};
+
+/**
+ * A mapping of component class models, keyed by name.
+ * @type {Object<String, Constructor<Component>>}
+ */
+const componentModelMap = {};
+
+/**
+ * A mapping of active component maps, keyed by the element components are assigned to.
+ * @type {WeakMap<Element, Object<String, Component>>}
+ */
+const elementComponentMap = new WeakMap();
 
 /**
  * Initialize a component instance on the given dom element.
@@ -8,7 +24,7 @@ const componentMap = {};
  */
 function initComponent(name, element) {
     /** @type {Function<Component>|undefined} **/
-    const componentModel = componentMap[name];
+    const componentModel = componentModelMap[name];
     if (componentModel === undefined) return;
 
     // Create our component instance
@@ -33,11 +49,10 @@ function initComponent(name, element) {
     }
     components[name].push(instance);
 
-    // Add to element listing
-    if (typeof element.components === 'undefined') {
-        element.components = {};
-    }
-    element.components[name] = instance;
+    // Add to element mapping
+    const elComponents = elementComponentMap.get(element) || {};
+    elComponents[name] = instance;
+    elementComponentMap.set(element, elComponents);
 }
 
 /**
@@ -125,7 +140,7 @@ export function init(parentElement = document) {
 export function register(mapping) {
     const keys = Object.keys(mapping);
     for (const key of keys) {
-        componentMap[camelToKebab(key)] = mapping[key];
+        componentModelMap[camelToKebab(key)] = mapping[key];
     }
 }
 
@@ -147,6 +162,17 @@ export function get(name = '') {
     return components[name] || [];
 }
 
+/**
+ * Get the first component, of the given name, that's assigned to the given element.
+ * @param {Element} element
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function firstOnElement(element, name) {
+    const elComponents = elementComponentMap.get(element) || {};
+    return elComponents[name] || null;
+}
+
 function camelToKebab(camelStr) {
     return camelStr.replace(/[A-Z]/g, (str, offset) =>  (offset > 0 ? '-' : '') + str.toLowerCase());
 }
\ No newline at end of file