Repository: symfony/ux
Pull request #1486 (2024-02-10 12:48:04)

[Twig][Live] Skip Twig 3.9 🚒  (do not set `use_yield = true` for now)
As Symfony Twig Bridge & Twig Bundle have decided to skip Twig 3.9 for now, we should do the same, as testing and/or maintaining a compatible version would be really hard without the bundle support.

https://github.com/symfony/twig-bridge/commit/2abddb106523c53aebfe075c04a9d01bc8cdc5b4 
https://github.com/symfony/twig-bundle/commit/23a02ff1693c2c76a4fd55eae83704048d1e93e7


Update: 

Twig 3.9 introduce a major change in the way templates are rendered, and to fully test/fix our compatibility, we need to wait Bridge and Bundle to allow Twig 3.9

In the meantime, please do not set "use_yield = true" as we are not ready yet.

This is something that has a major impact on TwigComponent and LiveComponent, and we'll probably need some changes on the CI / test suite to handle both modes.



---

Pull Request (compatibility - wip) : https://github.com/symfony/ux/pull/1487 (help welcomed)
Issue: https://github.com/symfony/ux/issues/1390 
Twig PR : https://github.com/twigphp/Twig/pull/3950


smnandre: I "revert" https://github.com/symfony/ux/pull/1478 in this PR

Please do **_not_** set "use_yield = true" while our compatibility is not OK.

This is something that has a major impact on TwigComponent and LiveComponent, and we'll probably need some changes on the CI to handle both modes.

cc @weaverryan @kbond 

Patch:
diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md
index 8903ea2e..b339b919 100644
--- a/src/LiveComponent/CHANGELOG.md
+++ b/src/LiveComponent/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.15.0
-
--   [BC BREAK] The `data-live-id` attribute was changed to `id`.
-
 ## 2.14.1
 
 -   Fixed a regression in the testing tools related to the default HTTP
@@ -11,10 +7,8 @@
 
 ## 2.14.0
 
--   [BC BREAK] DOM morphing changed from `morphdom` to `idiomorph`. As this is
-    a different morphing library, there may be some edge cases where the
-    morphing behavior is different.
 -   Add support for URL binding in `LiveProp`
+-   DOM morphing changed from `morphdom` to `idiomorph`
 -   Allow multiple `LiveListener` attributes on a single method
 -   Requests to LiveComponent are sent as POST by default
 -   Add method prop to AsLiveComponent to still allow GET requests, usage: `#[AsLiveComponent(method: 'get')]`
diff --git a/src/LiveComponent/assets/dist/Component/ElementDriver.d.ts b/src/LiveComponent/assets/dist/Component/ElementDriver.d.ts
index 776f4701..972773de 100644
--- a/src/LiveComponent/assets/dist/Component/ElementDriver.d.ts
+++ b/src/LiveComponent/assets/dist/Component/ElementDriver.d.ts
@@ -1,30 +1,31 @@
-import LiveControllerDefault from '../live_controller';
 export interface ElementDriver {
     getModelName(element: HTMLElement): string | null;
-    getComponentProps(): any;
-    getEventsToEmit(): Array<{
+    getComponentProps(rootElement: HTMLElement): any;
+    findChildComponentElement(id: string, element: HTMLElement): HTMLElement | null;
+    getKeyFromElement(element: HTMLElement): string | null;
+    getEventsToEmit(element: HTMLElement): Array<{
         event: string;
         data: any;
         target: string | null;
         componentName: string | null;
     }>;
-    getBrowserEventsToDispatch(): Array<{
+    getBrowserEventsToDispatch(element: HTMLElement): Array<{
         event: string;
         payload: any;
     }>;
 }
-export declare class StimulusElementDriver implements ElementDriver {
-    private readonly controller;
-    constructor(controller: LiveControllerDefault);
+export declare class StandardElementDriver implements ElementDriver {
     getModelName(element: HTMLElement): string | null;
-    getComponentProps(): any;
-    getEventsToEmit(): Array<{
+    getComponentProps(rootElement: HTMLElement): any;
+    findChildComponentElement(id: string, element: HTMLElement): HTMLElement | null;
+    getKeyFromElement(element: HTMLElement): string | null;
+    getEventsToEmit(element: HTMLElement): Array<{
         event: string;
         data: any;
         target: string | null;
         componentName: string | null;
     }>;
-    getBrowserEventsToDispatch(): Array<{
+    getBrowserEventsToDispatch(element: HTMLElement): Array<{
         event: string;
         payload: any;
     }>;
diff --git a/src/LiveComponent/assets/dist/Component/index.d.ts b/src/LiveComponent/assets/dist/Component/index.d.ts
index 9c8b44ee..7470b6e9 100644
--- a/src/LiveComponent/assets/dist/Component/index.d.ts
+++ b/src/LiveComponent/assets/dist/Component/index.d.ts
@@ -3,14 +3,17 @@ import ValueStore from './ValueStore';
 import { ElementDriver } from './ElementDriver';
 import { PluginInterface } from './plugins/PluginInterface';
 import BackendResponse from '../Backend/BackendResponse';
+import { ModelBinding } from '../Directive/get_model_binding';
+export type ComponentFinder = (currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null) => Component[];
 export default class Component {
     readonly element: HTMLElement;
     readonly name: string;
     readonly listeners: Map<string, string[]>;
+    private readonly componentFinder;
     private backend;
-    readonly elementDriver: ElementDriver;
+    private readonly elementDriver;
     id: string | null;
-    fingerprint: string;
+    fingerprint: string | null;
     readonly valueStore: ValueStore;
     private readonly unsyncedInputsTracker;
     private hooks;
@@ -22,11 +25,14 @@ export default class Component {
     private requestDebounceTimeout;
     private nextRequestPromise;
     private nextRequestPromiseResolve;
+    private children;
+    private parent;
     private externalMutationTracker;
     constructor(element: HTMLElement, name: string, props: any, listeners: Array<{
         event: string;
         action: string;
-    }>, id: string | null, backend: BackendInterface, elementDriver: ElementDriver);
+    }>, componentFinder: ComponentFinder, fingerprint: string | null, id: string | null, backend: BackendInterface, elementDriver: ElementDriver);
+    _swapBackend(backend: BackendInterface): void;
     addPlugin(plugin: PluginInterface): void;
     connect(): void;
     disconnect(): void;
@@ -38,11 +44,17 @@ export default class Component {
     files(key: string, input: HTMLInputElement): void;
     render(): Promise<BackendResponse>;
     getUnsyncedModels(): string[];
+    addChild(child: Component, modelBindings?: ModelBinding[]): void;
+    removeChild(child: Component): void;
+    getParent(): Component | null;
+    getChildren(): Map<string, Component>;
     emit(name: string, data: any, onlyMatchingComponentsNamed?: string | null): void;
     emitUp(name: string, data: any, onlyMatchingComponentsNamed?: string | null): void;
     emitSelf(name: string, data: any): void;
     private performEmit;
     private doEmit;
+    updateFromNewElementFromParentRender(toEl: HTMLElement): boolean;
+    onChildComponentModelUpdate(modelName: string, value: any, childComponent: Component): void;
     private isTurboEnabled;
     private tryStartingRequest;
     private performRequest;
@@ -51,7 +63,7 @@ export default class Component {
     private clearRequestDebounceTimeout;
     private debouncedStartRequest;
     private renderError;
+    private getChildrenFingerprints;
     private resetPromise;
-    _updateFromParentProps(props: any): void;
 }
 export declare function proxifyComponent(component: Component): Component;
diff --git a/src/LiveComponent/assets/dist/Component/plugins/ChildComponentPlugin.d.ts b/src/LiveComponent/assets/dist/Component/plugins/ChildComponentPlugin.d.ts
deleted file mode 100644
index f5df0e82..00000000
--- a/src/LiveComponent/assets/dist/Component/plugins/ChildComponentPlugin.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import Component from '../../Component';
-import { PluginInterface } from './PluginInterface';
-export default class implements PluginInterface {
-    private readonly component;
-    private parentModelBindings;
-    constructor(component: Component);
-    attachToComponent(component: Component): void;
-    private getChildrenFingerprints;
-    private notifyParentModelChange;
-    private getChildren;
-}
diff --git a/src/LiveComponent/assets/dist/ComponentRegistry.d.ts b/src/LiveComponent/assets/dist/ComponentRegistry.d.ts
index d1195475..e9778dd6 100644
--- a/src/LiveComponent/assets/dist/ComponentRegistry.d.ts
+++ b/src/LiveComponent/assets/dist/ComponentRegistry.d.ts
@@ -1,8 +1,9 @@
 import Component from './Component';
-export declare const resetRegistry: () => void;
-export declare const registerComponent: (component: Component) => void;
-export declare const unregisterComponent: (component: Component) => void;
-export declare const getComponent: (element: HTMLElement) => Promise<Component>;
-export declare const findComponents: (currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null) => Component[];
-export declare const findChildren: (currentComponent: Component) => Component[];
-export declare const findParent: (currentComponent: Component) => Component | null;
+export default class {
+    private componentMapByElement;
+    private componentMapByComponent;
+    registerComponent(element: HTMLElement, component: Component): void;
+    unregisterComponent(component: Component): void;
+    getComponent(element: HTMLElement): Promise<Component>;
+    findComponents(currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null): Component[];
+}
diff --git a/src/LiveComponent/assets/dist/Util/getElementAsTagText.d.ts b/src/LiveComponent/assets/dist/Util/getElementAsTagText.d.ts
deleted file mode 100644
index dbd343f3..00000000
--- a/src/LiveComponent/assets/dist/Util/getElementAsTagText.d.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export default function getElementAsTagText(element: HTMLElement): string;
diff --git a/src/LiveComponent/assets/dist/dom_utils.d.ts b/src/LiveComponent/assets/dist/dom_utils.d.ts
index a897f086..5286cd11 100644
--- a/src/LiveComponent/assets/dist/dom_utils.d.ts
+++ b/src/LiveComponent/assets/dist/dom_utils.d.ts
@@ -8,3 +8,4 @@ export declare function getModelDirectiveFromElement(element: HTMLElement, throw
 export declare function elementBelongsToThisComponent(element: Element, component: Component): boolean;
 export declare function cloneHTMLElement(element: HTMLElement): HTMLElement;
 export declare function htmlToElement(html: string): HTMLElement;
+export declare function getElementAsTagText(element: HTMLElement): string;
diff --git a/src/LiveComponent/assets/dist/live_controller.d.ts b/src/LiveComponent/assets/dist/live_controller.d.ts
index fa533b60..9bb12a6e 100644
--- a/src/LiveComponent/assets/dist/live_controller.d.ts
+++ b/src/LiveComponent/assets/dist/live_controller.d.ts
@@ -1,8 +1,8 @@
 import { Controller } from '@hotwired/stimulus';
 import Component from './Component';
-import { BackendInterface } from './Backend/Backend';
+import ComponentRegistry from './ComponentRegistry';
 export { Component };
-export { getComponent } from './ComponentRegistry';
+export declare const getComponent: (element: HTMLElement) => Promise<Component>;
 export interface LiveEvent extends CustomEvent {
     detail: {
         controller: LiveController;
@@ -17,31 +17,17 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     static values: {
         name: StringConstructor;
         url: StringConstructor;
-        props: {
-            type: ObjectConstructor;
-            default: {};
-        };
-        propsUpdatedFromParent: {
-            type: ObjectConstructor;
-            default: {};
-        };
+        props: ObjectConstructor;
         csrf: StringConstructor;
         listeners: {
             type: ArrayConstructor;
             default: never[];
         };
-        eventsToEmit: {
-            type: ArrayConstructor;
-            default: never[];
-        };
-        eventsToDispatch: {
-            type: ArrayConstructor;
-            default: never[];
-        };
         debounce: {
             type: NumberConstructor;
             default: number;
         };
+        id: StringConstructor;
         fingerprint: {
             type: StringConstructor;
             default: string;
@@ -58,22 +44,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     readonly nameValue: string;
     readonly urlValue: string;
     readonly propsValue: any;
-    propsUpdatedFromParentValue: any;
     readonly csrfValue: string;
     readonly listenersValue: Array<{
         event: string;
         action: string;
     }>;
-    readonly eventsToEmitValue: Array<{
-        event: string;
-        data: any;
-        target: string | null;
-        componentName: string | null;
-    }>;
-    readonly eventsToDispatchValue: Array<{
-        event: string;
-        payload: any;
-    }>;
     readonly hasDebounceValue: boolean;
     readonly debounceValue: number;
     readonly fingerprintValue: string;
@@ -84,31 +59,26 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         };
     };
     private proxiedComponent;
-    private mutationObserver;
     component: Component;
     pendingActionTriggerModelElement: HTMLElement | null;
     private elementEventListeners;
     private pendingFiles;
-    static backendFactory: (controller: LiveControllerDefault) => BackendInterface;
+    static componentRegistry: ComponentRegistry;
     initialize(): void;
     connect(): void;
     disconnect(): void;
     update(event: any): void;
     action(event: any): void;
+    $render(): Promise<import("./Backend/BackendResponse").default>;
     emit(event: Event): void;
     emitUp(event: Event): void;
     emitSelf(event: Event): void;
-    $render(): Promise<import("./Backend/BackendResponse").default>;
-    $updateModel(model: string, value: any, shouldRender?: boolean, debounce?: number | boolean): Promise<import("./Backend/BackendResponse").default>;
-    propsUpdatedFromParentValueChanged(): void;
-    fingerprintValueChanged(): void;
     private getEmitDirectives;
-    private createComponent;
-    private connectComponent;
-    private disconnectComponent;
+    $updateModel(model: string, value: any, shouldRender?: boolean, debounce?: number | boolean): Promise<import("./Backend/BackendResponse").default>;
     private handleInputEvent;
     private handleChangeEvent;
     private updateModelFromElementEvent;
+    handleConnectedControllerEvent(event: LiveEvent): void;
+    handleDisconnectedChildControllerEvent(event: LiveEvent): void;
     private dispatchEvent;
-    private onMutations;
 }
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 2968fd82..5663876c 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -156,90 +156,6 @@ function normalizeModelName(model) {
         .join('.'));
 }
 
-function getElementAsTagText(element) {
-    return element.innerHTML
-        ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
-        : element.outerHTML;
-}
-
-let componentMapByElement = new WeakMap();
-let componentMapByComponent = new Map();
-const registerComponent = function (component) {
-    componentMapByElement.set(component.element, component);
-    componentMapByComponent.set(component, component.name);
-};
-const unregisterComponent = function (component) {
-    componentMapByElement.delete(component.element);
-    componentMapByComponent.delete(component);
-};
-const getComponent = function (element) {
-    return new Promise((resolve, reject) => {
-        let count = 0;
-        const maxCount = 10;
-        const interval = setInterval(() => {
-            const component = componentMapByElement.get(element);
-            if (component) {
-                clearInterval(interval);
-                resolve(component);
-            }
-            count++;
-            if (count > maxCount) {
-                clearInterval(interval);
-                reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
-            }
-        }, 5);
-    });
-};
-const findComponents = function (currentComponent, onlyParents, onlyMatchName) {
-    const components = [];
-    componentMapByComponent.forEach((componentName, component) => {
-        if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) {
-            return;
-        }
-        if (onlyMatchName && componentName !== onlyMatchName) {
-            return;
-        }
-        components.push(component);
-    });
-    return components;
-};
-const findChildren = function (currentComponent) {
-    const children = [];
-    componentMapByComponent.forEach((componentName, component) => {
-        if (currentComponent === component) {
-            return;
-        }
-        if (!currentComponent.element.contains(component.element)) {
-            return;
-        }
-        let foundChildComponent = false;
-        componentMapByComponent.forEach((childComponentName, childComponent) => {
-            if (foundChildComponent) {
-                return;
-            }
-            if (childComponent === component) {
-                return;
-            }
-            if (childComponent.element.contains(component.element)) {
-                foundChildComponent = true;
-            }
-        });
-        children.push(component);
-    });
-    return children;
-};
-const findParent = function (currentComponent) {
-    let parentElement = currentComponent.element.parentElement;
-    while (parentElement) {
-        const component = componentMapByElement.get(parentElement);
-        if (component) {
-            return component;
-        }
-        parentElement = parentElement.parentElement;
-    }
-    return null;
-};
-
 function getValueFromElement(element, valueStore) {
     if (element instanceof HTMLInputElement) {
         if (element.type === 'checkbox') {
@@ -362,7 +278,7 @@ function elementBelongsToThisComponent(element, component) {
         return false;
     }
     let foundChildComponent = false;
-    findChildren(component).forEach((childComponent) => {
+    component.getChildren().forEach((childComponent) => {
         if (foundChildComponent) {
             return;
         }
@@ -395,6 +311,11 @@ function htmlToElement(html) {
     }
     return child;
 }
+function getElementAsTagText(element) {
+    return element.innerHTML
+        ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
+        : element.outerHTML;
+}
 const getMultipleCheckboxValue = function (element, currentValues) {
     const finalValues = [...currentValues];
     const value = inputValue(element);
@@ -1354,28 +1275,10 @@ function normalizeAttributesForComparison(element) {
     });
 }
 
-const syncAttributes = function (fromEl, toEl) {
-    for (let i = 0; i < fromEl.attributes.length; i++) {
-        const attr = fromEl.attributes[i];
-        toEl.setAttribute(attr.name, attr.value);
-    }
-};
-function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) {
-    const preservedOriginalElements = [];
-    rootToElement.querySelectorAll('[data-live-preserve]').forEach((newElement) => {
-        const id = newElement.id;
-        if (!id) {
-            throw new Error('The data-live-preserve attribute requires an id attribute to be set on the element');
-        }
-        const oldElement = rootFromElement.querySelector(`#${id}`);
-        if (!(oldElement instanceof HTMLElement)) {
-            throw new Error(`The element with id "${id}" was not found in the original HTML`);
-        }
-        const clonedOldElement = cloneHTMLElement(oldElement);
-        preservedOriginalElements.push(oldElement);
-        oldElement.replaceWith(clonedOldElement);
-        newElement.removeAttribute('data-live-preserve');
-        syncAttributes(newElement, oldElement);
+function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, childComponents, findChildComponent, getKeyFromElement, externalMutationTracker) {
+    const childComponentMap = new Map();
+    childComponents.forEach((childComponent) => {
+        childComponentMap.set(childComponent.element, childComponent);
     });
     Idiomorph.morph(rootFromElement, rootToElement, {
         callbacks: {
@@ -1386,6 +1289,15 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
                 if (fromEl === rootFromElement) {
                     return true;
                 }
+                let idChanged = false;
+                if (fromEl.hasAttribute('data-live-id')) {
+                    if (fromEl.getAttribute('data-live-id') !== toEl.getAttribute('data-live-id')) {
+                        for (const child of fromEl.children) {
+                            child.setAttribute('parent-live-id-changed', '');
+                        }
+                        idChanged = true;
+                    }
+                }
                 if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
                     if (typeof fromEl.__x !== 'undefined') {
                         if (!window.Alpine) {
@@ -1396,6 +1308,10 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
                         }
                         window.Alpine.morph(fromEl.__x, toEl);
                     }
+                    if (childComponentMap.has(fromEl)) {
+                        const childComponent = childComponentMap.get(fromEl);
+                        return !childComponent.updateFromNewElementFromParentRender(toEl) && idChanged;
+                    }
                     if (externalMutationTracker.wasElementAdded(fromEl)) {
                         fromEl.insertAdjacentElement('afterend', toEl);
                         return false;
@@ -1417,7 +1333,11 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
                         }
                     }
                 }
-                if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
+                if (fromEl.hasAttribute('parent-live-id-changed')) {
+                    fromEl.removeAttribute('parent-live-id-changed');
+                    return true;
+                }
+                if (fromEl.hasAttribute('data-skip-morph')) {
                     fromEl.innerHTML = toEl.innerHTML;
                     return true;
                 }
@@ -1437,12 +1357,14 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
             },
         },
     });
-    preservedOriginalElements.forEach((oldElement) => {
-        const newElement = rootFromElement.querySelector(`#${oldElement.id}`);
-        if (!(newElement instanceof HTMLElement)) {
-            throw new Error('Missing preserved element');
+    childComponentMap.forEach((childComponent, element) => {
+        var _a;
+        const childComponentInResult = findChildComponent((_a = childComponent.id) !== null && _a !== void 0 ? _a : '', rootFromElement);
+        if (null === childComponentInResult || element === childComponentInResult) {
+            return;
         }
-        newElement.replaceWith(oldElement);
+        childComponentInResult === null || childComponentInResult === void 0 ? void 0 : childComponentInResult.replaceWith(element);
+        childComponent.updateFromNewElementFromParentRender(childComponentInResult);
     });
 }
 
@@ -1894,20 +1816,29 @@ class ExternalMutationTracker {
     }
 }
 
+class ChildComponentWrapper {
+    constructor(component, modelBindings) {
+        this.component = component;
+        this.modelBindings = modelBindings;
+    }
+}
 class Component {
-    constructor(element, name, props, listeners, id, backend, elementDriver) {
-        this.fingerprint = '';
+    constructor(element, name, props, listeners, componentFinder, fingerprint, id, backend, elementDriver) {
         this.defaultDebounce = 150;
         this.backendRequest = null;
         this.pendingActions = [];
         this.pendingFiles = {};
         this.isRequestPending = false;
         this.requestDebounceTimeout = null;
+        this.children = new Map();
+        this.parent = null;
         this.element = element;
         this.name = name;
+        this.componentFinder = componentFinder;
         this.backend = backend;
         this.elementDriver = elementDriver;
         this.id = id;
+        this.fingerprint = fingerprint;
         this.listeners = new Map();
         listeners.forEach((listener) => {
             var _a;
@@ -1922,18 +1853,20 @@ class Component {
         this.resetPromise();
         this.externalMutationTracker = new ExternalMutationTracker(this.element, (element) => elementBelongsToThisComponent(element, this));
         this.externalMutationTracker.start();
+        this.onChildComponentModelUpdate = this.onChildComponentModelUpdate.bind(this);
+    }
+    _swapBackend(backend) {
+        this.backend = backend;
     }
     addPlugin(plugin) {
         plugin.attachToComponent(this);
     }
     connect() {
-        registerComponent(this);
         this.hooks.triggerHook('connect', this);
         this.unsyncedInputsTracker.activate();
         this.externalMutationTracker.start();
     }
     disconnect() {
-        unregisterComponent(this);
         this.hooks.triggerHook('disconnect', this);
         this.clearRequestDebounceTimeout();
         this.unsyncedInputsTracker.deactivate();
@@ -1986,6 +1919,32 @@ class Component {
     getUnsyncedModels() {
         return this.unsyncedInputsTracker.getUnsyncedModels();
     }
+    addChild(child, modelBindings = []) {
+        if (!child.id) {
+            throw new Error('Children components must have an id.');
+        }
+        this.children.set(child.id, new ChildComponentWrapper(child, modelBindings));
+        child.parent = this;
+        child.on('model:set', this.onChildComponentModelUpdate);
+    }
+    removeChild(child) {
+        if (!child.id) {
+            throw new Error('Children components must have an id.');
+        }
+        this.children.delete(child.id);
+        child.parent = null;
+        child.off('model:set', this.onChildComponentModelUpdate);
+    }
+    getParent() {
+        return this.parent;
+    }
+    getChildren() {
+        const children = new Map();
+        this.children.forEach((childComponent, id) => {
+            children.set(id, childComponent.component);
+        });
+        return children;
+    }
     emit(name, data, onlyMatchingComponentsNamed = null) {
         return this.performEmit(name, data, false, onlyMatchingComponentsNamed);
     }
@@ -1996,7 +1955,7 @@ class Component {
         return this.doEmit(name, data);
     }
     performEmit(name, data, emitUp, matchingName) {
-        const components = findComponents(this, emitUp, matchingName);
+        const components = this.componentFinder(this, emitUp, matchingName);
         components.forEach((component) => {
             component.doEmit(name, data);
         });
@@ -2010,6 +1969,37 @@ class Component {
             this.action(action, data, 1);
         });
     }
+    updateFromNewElementFromParentRender(toEl) {
+        const props = this.elementDriver.getComponentProps(toEl);
+        if (props === null) {
+            return false;
+        }
+        const isChanged = this.valueStore.storeNewPropsFromParent(props);
+        const fingerprint = toEl.dataset.liveFingerprintValue;
+        if (fingerprint !== undefined) {
+            this.fingerprint = fingerprint;
+        }
+        if (isChanged) {
+            this.render();
+        }
+        return isChanged;
+    }
+    onChildComponentModelUpdate(modelName, value, childComponent) {
+        if (!childComponent.id) {
+            throw new Error('Missing id');
+        }
+        const childWrapper = this.children.get(childComponent.id);
+        if (!childWrapper) {
+            throw new Error('Missing child');
+        }
+        childWrapper.modelBindings.forEach((modelBinding) => {
+            const childModelName = modelBinding.innerModelName || 'value';
+            if (childModelName !== modelName) {
+                return;
+            }
+            this.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce);
+        });
+    }
     isTurboEnabled() {
         return typeof Turbo !== 'undefined' && !this.element.closest('[data-turbo="false"]');
     }
@@ -2030,16 +2020,7 @@ class Component {
                 filesToSend[key] = value.files;
             }
         }
-        const requestConfig = {
-            props: this.valueStore.getOriginalProps(),
-            actions: this.pendingActions,
-            updated: this.valueStore.getDirtyProps(),
-            children: {},
-            updatedPropsFromParent: this.valueStore.getUpdatedPropsFromParent(),
-            files: filesToSend,
-        };
-        this.hooks.triggerHook('request:started', requestConfig);
-        this.backendRequest = this.backend.makeRequest(requestConfig.props, requestConfig.actions, requestConfig.updated, requestConfig.children, requestConfig.updatedPropsFromParent, requestConfig.files);
+        this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints(), this.valueStore.getUpdatedPropsFromParent(), filesToSend);
         this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
         this.pendingActions = [];
         this.valueStore.flushDirtyPropsToPending();
@@ -2103,14 +2084,14 @@ class Component {
             console.error('There was a problem with the component HTML returned:');
             throw error;
         }
+        const newProps = this.elementDriver.getComponentProps(newElement);
+        this.valueStore.reinitializeAllProps(newProps);
+        const eventsToEmit = this.elementDriver.getEventsToEmit(newElement);
+        const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch(newElement);
         this.externalMutationTracker.handlePendingChanges();
         this.externalMutationTracker.stop();
-        executeMorphdom(this.element, newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), this.externalMutationTracker);
+        executeMorphdom(this.element, newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), Array.from(this.getChildren().values()), this.elementDriver.findChildComponentElement, this.elementDriver.getKeyFromElement, this.externalMutationTracker);
         this.externalMutationTracker.start();
-        const newProps = this.elementDriver.getComponentProps();
-        this.valueStore.reinitializeAllProps(newProps);
-        const eventsToEmit = this.elementDriver.getEventsToEmit();
-        const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch();
         Object.keys(modifiedModelValues).forEach((modelName) => {
             this.valueStore.set(modelName, modifiedModelValues[modelName]);
         });
@@ -2199,17 +2180,25 @@ class Component {
         });
         modal.focus();
     }
+    getChildrenFingerprints() {
+        const fingerprints = {};
+        this.children.forEach((childComponent) => {
+            const child = childComponent.component;
+            if (!child.id) {
+                throw new Error('missing id');
+            }
+            fingerprints[child.id] = {
+                fingerprint: child.fingerprint,
+                tag: child.element.tagName.toLowerCase(),
+            };
+        });
+        return fingerprints;
+    }
     resetPromise() {
         this.nextRequestPromise = new Promise((resolve) => {
             this.nextRequestPromiseResolve = resolve;
         });
     }
-    _updateFromParentProps(props) {
-        const isChanged = this.valueStore.storeNewPropsFromParent(props);
-        if (isChanged) {
-            this.render();
-        }
-    }
 }
 function proxifyComponent(component) {
     return new Proxy(component, {
@@ -2347,10 +2336,7 @@ class Backend {
     }
 }
 
-class StimulusElementDriver {
-    constructor(controller) {
-        this.controller = controller;
-    }
+class StandardElementDriver {
     getModelName(element) {
         const modelDirective = getModelDirectiveFromElement(element, false);
         if (!modelDirective) {
@@ -2358,14 +2344,26 @@ class StimulusElementDriver {
         }
         return modelDirective.action;
     }
-    getComponentProps() {
-        return this.controller.propsValue;
+    getComponentProps(rootElement) {
+        var _a;
+        const propsJson = (_a = rootElement.dataset.livePropsValue) !== null && _a !== void 0 ? _a : '{}';
+        return JSON.parse(propsJson);
+    }
+    findChildComponentElement(id, element) {
+        return element.querySelector(`[data-live-id=${id}]`);
     }
-    getEventsToEmit() {
-        return this.controller.eventsToEmitValue;
+    getKeyFromElement(element) {
+        return element.dataset.liveId || null;
     }
-    getBrowserEventsToDispatch() {
-        return this.controller.eventsToDispatchValue;
+    getEventsToEmit(element) {
+        var _a;
+        const eventsJson = (_a = element.dataset.liveEmit) !== null && _a !== void 0 ? _a : '[]';
+        return JSON.parse(eventsJson);
+    }
+    getBrowserEventsToDispatch(element) {
+        var _a;
+        const eventsJson = (_a = element.dataset.liveBrowserDispatch) !== null && _a !== void 0 ? _a : '[]';
+        return JSON.parse(eventsJson);
     }
 }
 
@@ -2478,7 +2476,7 @@ class LoadingPlugin {
     }
     getLoadingDirectives(component, element) {
         const loadingDirectives = [];
-        let matchingElements = [...Array.from(element.querySelectorAll('[data-loading]'))];
+        let matchingElements = [...element.querySelectorAll('[data-loading]')];
         matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
         if (element.hasAttribute('data-loading')) {
             matchingElements = [element, ...matchingElements];
@@ -2743,6 +2741,53 @@ function getModelBinding (modelDirective) {
     };
 }
 
+class ComponentRegistry {
+    constructor() {
+        this.componentMapByElement = new WeakMap();
+        this.componentMapByComponent = new Map();
+    }
+    registerComponent(element, component) {
+        this.componentMapByElement.set(element, component);
+        this.componentMapByComponent.set(component, component.name);
+    }
+    unregisterComponent(component) {
+        this.componentMapByElement.delete(component.element);
+        this.componentMapByComponent.delete(component);
+    }
+    getComponent(element) {
+        return new Promise((resolve, reject) => {
+            let count = 0;
+            const maxCount = 10;
+            const interval = setInterval(() => {
+                const component = this.componentMapByElement.get(element);
+                if (component) {
+                    clearInterval(interval);
+                    resolve(component);
+                }
+                count++;
+                if (count > maxCount) {
+                    clearInterval(interval);
+                    reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
+                }
+            }, 5);
+        });
+    }
+    findComponents(currentComponent, onlyParents, onlyMatchName) {
+        const components = [];
+        this.componentMapByComponent.forEach((componentName, component) => {
+            if (onlyParents &&
+                (currentComponent === component || !component.element.contains(currentComponent.element))) {
+                return;
+            }
+            if (onlyMatchName && componentName !== onlyMatchName) {
+                return;
+            }
+            components.push(component);
+        });
+        return components;
+    }
+}
+
 function isValueEmpty(value) {
     if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) {
         return true;
@@ -2864,52 +2909,7 @@ class QueryStringPlugin {
     }
 }
 
-class ChildComponentPlugin {
-    constructor(component) {
-        this.parentModelBindings = [];
-        this.component = component;
-        const modelDirectives = getAllModelDirectiveFromElements(this.component.element);
-        this.parentModelBindings = modelDirectives.map(getModelBinding);
-    }
-    attachToComponent(component) {
-        component.on('request:started', (requestData) => {
-            requestData.children = this.getChildrenFingerprints();
-        });
-        component.on('model:set', (model, value) => {
-            this.notifyParentModelChange(model, value);
-        });
-    }
-    getChildrenFingerprints() {
-        const fingerprints = {};
-        this.getChildren().forEach((child) => {
-            if (!child.id) {
-                throw new Error('missing id');
-            }
-            fingerprints[child.id] = {
-                fingerprint: child.fingerprint,
-                tag: child.element.tagName.toLowerCase(),
-            };
-        });
-        return fingerprints;
-    }
-    notifyParentModelChange(modelName, value) {
-        const parentComponent = findParent(this.component);
-        if (!parentComponent) {
-            return;
-        }
-        this.parentModelBindings.forEach((modelBinding) => {
-            const childModelName = modelBinding.innerModelName || 'value';
-            if (childModelName !== modelName) {
-                return;
-            }
-            parentComponent.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce);
-        });
-    }
-    getChildren() {
-        return findChildren(this.component);
-    }
-}
-
+const getComponent = (element) => LiveControllerDefault.componentRegistry.getComponent(element);
 class LiveControllerDefault extends Controller {
     constructor() {
         super(...arguments);
@@ -2917,22 +2917,46 @@ class LiveControllerDefault extends Controller {
         this.elementEventListeners = [
             { event: 'input', callback: (event) => this.handleInputEvent(event) },
             { event: 'change', callback: (event) => this.handleChangeEvent(event) },
+            { event: 'live:connect', callback: (event) => this.handleConnectedControllerEvent(event) },
         ];
         this.pendingFiles = {};
     }
     initialize() {
-        this.mutationObserver = new MutationObserver(this.onMutations.bind(this));
-        this.createComponent();
+        this.handleDisconnectedChildControllerEvent = this.handleDisconnectedChildControllerEvent.bind(this);
+        const id = this.element.dataset.liveId || null;
+        this.component = new Component(this.element, this.nameValue, this.propsValue, this.listenersValue, (currentComponent, onlyParents, onlyMatchName) => LiveControllerDefault.componentRegistry.findComponents(currentComponent, onlyParents, onlyMatchName), this.fingerprintValue, id, new Backend(this.urlValue, this.requestMethodValue, this.csrfValue), new StandardElementDriver());
+        this.proxiedComponent = proxifyComponent(this.component);
+        this.element.__component = this.proxiedComponent;
+        if (this.hasDebounceValue) {
+            this.component.defaultDebounce = this.debounceValue;
+        }
+        const plugins = [
+            new LoadingPlugin(),
+            new ValidatedFieldsPlugin(),
+            new PageUnloadingPlugin(),
+            new PollingPlugin(),
+            new SetValueOntoModelFieldsPlugin(),
+            new QueryStringPlugin(this.queryMappingValue),
+        ];
+        plugins.forEach((plugin) => {
+            this.component.addPlugin(plugin);
+        });
     }
     connect() {
-        this.connectComponent();
-        this.mutationObserver.observe(this.element, {
-            attributes: true,
+        LiveControllerDefault.componentRegistry.registerComponent(this.element, this.component);
+        this.component.connect();
+        this.elementEventListeners.forEach(({ event, callback }) => {
+            this.component.element.addEventListener(event, callback);
         });
+        this.dispatchEvent('connect');
     }
     disconnect() {
-        this.disconnectComponent();
-        this.mutationObserver.disconnect();
+        LiveControllerDefault.componentRegistry.unregisterComponent(this.component);
+        this.component.disconnect();
+        this.elementEventListeners.forEach(({ event, callback }) => {
+            this.component.element.removeEventListener(event, callback);
+        });
+        this.dispatchEvent('disconnect');
     }
     update(event) {
         if (event.type === 'input' || event.type === 'change') {
@@ -2990,6 +3014,9 @@ class LiveControllerDefault extends Controller {
             }
         });
     }
+    $render() {
+        return this.component.render();
+    }
     emit(event) {
         this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
             this.component.emit(name, data, nameMatch);
@@ -3005,18 +3032,6 @@ class LiveControllerDefault extends Controller {
             this.component.emitSelf(name, data);
         });
     }
-    $render() {
-        return this.component.render();
-    }
-    $updateModel(model, value, shouldRender = true, debounce = true) {
-        return this.component.set(model, value, shouldRender, debounce);
-    }
-    propsUpdatedFromParentValueChanged() {
-        this.component._updateFromParentProps(this.propsUpdatedFromParentValue);
-    }
-    fingerprintValueChanged() {
-        this.component.fingerprint = this.fingerprintValue;
-    }
     getEmitDirectives(event) {
         const element = event.currentTarget;
         if (!element.dataset.event) {
@@ -3044,43 +3059,8 @@ class LiveControllerDefault extends Controller {
         });
         return emits;
     }
-    createComponent() {
-        const id = this.element.id || null;
-        this.component = new Component(this.element, this.nameValue, this.propsValue, this.listenersValue, id, LiveControllerDefault.backendFactory(this), new StimulusElementDriver(this));
-        this.proxiedComponent = proxifyComponent(this.component);
-        this.element.__component = this.proxiedComponent;
-        if (this.hasDebounceValue) {
-            this.component.defaultDebounce = this.debounceValue;
-        }
-        const plugins = [
-            new LoadingPlugin(),
-            new ValidatedFieldsPlugin(),
-            new PageUnloadingPlugin(),
-            new PollingPlugin(),
-            new SetValueOntoModelFieldsPlugin(),
-            new QueryStringPlugin(this.queryMappingValue),
-            new ChildComponentPlugin(this.component),
-        ];
-        plugins.forEach((plugin) => {
-            this.component.addPlugin(plugin);
-        });
-    }
-    connectComponent() {
-        this.component.connect();
-        this.mutationObserver.observe(this.element, {
-            attributes: true,
-        });
-        this.elementEventListeners.forEach(({ event, callback }) => {
-            this.component.element.addEventListener(event, callback);
-        });
-        this.dispatchEvent('connect');
-    }
-    disconnectComponent() {
-        this.component.disconnect();
-        this.elementEventListeners.forEach(({ event, callback }) => {
-            this.component.element.removeEventListener(event, callback);
-        });
-        this.dispatchEvent('disconnect');
+    $updateModel(model, value, shouldRender = true, debounce = true) {
+        return this.component.set(model, value, shouldRender, debounce);
     }
     handleInputEvent(event) {
         const target = event.target;
@@ -3141,37 +3121,45 @@ class LiveControllerDefault extends Controller {
         const finalValue = getValueFromElement(element, this.component.valueStore);
         this.component.set(modelBinding.modelName, finalValue, modelBinding.shouldRender, modelBinding.debounce);
     }
+    handleConnectedControllerEvent(event) {
+        if (event.target === this.element) {
+            return;
+        }
+        const childController = event.detail.controller;
+        if (childController.component.getParent()) {
+            return;
+        }
+        const modelDirectives = getAllModelDirectiveFromElements(childController.element);
+        const modelBindings = modelDirectives.map(getModelBinding);
+        this.component.addChild(childController.component, modelBindings);
+        childController.element.addEventListener('live:disconnect', this.handleDisconnectedChildControllerEvent);
+    }
+    handleDisconnectedChildControllerEvent(event) {
+        const childController = event.detail.controller;
+        childController.element.removeEventListener('live:disconnect', this.handleDisconnectedChildControllerEvent);
+        if (childController.component.getParent() !== this.component) {
+            return;
+        }
+        this.component.removeChild(childController.component);
+    }
     dispatchEvent(name, detail = {}, canBubble = true, cancelable = false) {
         detail.controller = this;
         detail.component = this.proxiedComponent;
         this.dispatch(name, { detail, prefix: 'live', cancelable, bubbles: canBubble });
     }
-    onMutations(mutations) {
-        mutations.forEach((mutation) => {
-            if (mutation.type === 'attributes' &&
-                mutation.attributeName === 'id' &&
-                this.element.id !== this.component.id) {
-                this.disconnectComponent();
-                this.createComponent();
-                this.connectComponent();
-            }
-        });
-    }
 }
 LiveControllerDefault.values = {
     name: String,
     url: String,
-    props: { type: Object, default: {} },
-    propsUpdatedFromParent: { type: Object, default: {} },
+    props: Object,
     csrf: String,
     listeners: { type: Array, default: [] },
-    eventsToEmit: { type: Array, default: [] },
-    eventsToDispatch: { type: Array, default: [] },
     debounce: { type: Number, default: 150 },
+    id: String,
     fingerprint: { type: String, default: '' },
     requestMethod: { type: String, default: 'post' },
     queryMapping: { type: Object, default: {} },
 };
-LiveControllerDefault.backendFactory = (controller) => new Backend(controller.urlValue, controller.requestMethodValue, controller.csrfValue);
+LiveControllerDefault.componentRegistry = new ComponentRegistry();
 
 export { Component, LiveControllerDefault as default, getComponent };
diff --git a/src/LiveComponent/assets/dist/morphdom.d.ts b/src/LiveComponent/assets/dist/morphdom.d.ts
index 1b157b0f..20d83e7b 100644
--- a/src/LiveComponent/assets/dist/morphdom.d.ts
+++ b/src/LiveComponent/assets/dist/morphdom.d.ts
@@ -1,2 +1,3 @@
+import Component from './Component';
 import ExternalMutationTracker from './Rendering/ExternalMutationTracker';
-export declare function executeMorphdom(rootFromElement: HTMLElement, rootToElement: HTMLElement, modifiedFieldElements: Array<HTMLElement>, getElementValue: (element: HTMLElement) => any, externalMutationTracker: ExternalMutationTracker): void;
+export declare function executeMorphdom(rootFromElement: HTMLElement, rootToElement: HTMLElement, modifiedFieldElements: Array<HTMLElement>, getElementValue: (element: HTMLElement) => any, childComponents: Component[], findChildComponent: (id: string, element: HTMLElement) => HTMLElement | null, getKeyFromElement: (element: HTMLElement) => string | null, externalMutationTracker: ExternalMutationTracker): void;
diff --git a/src/LiveComponent/assets/src/Component/ElementDriver.ts b/src/LiveComponent/assets/src/Component/ElementDriver.ts
index c3a8204e..0be5bdf7 100644
--- a/src/LiveComponent/assets/src/Component/ElementDriver.ts
+++ b/src/LiveComponent/assets/src/Component/ElementDriver.ts
@@ -1,29 +1,32 @@
 import {getModelDirectiveFromElement} from '../dom_utils';
-import LiveControllerDefault from '../live_controller';
 
 export interface ElementDriver {
     getModelName(element: HTMLElement): string|null;
 
-    getComponentProps(): any;
+    getComponentProps(rootElement: HTMLElement): any;
+
+    /**
+     * Given an HtmlElement and a child id, find the root element for that child.
+     */
+    findChildComponentElement(id: string, element: HTMLElement): HTMLElement|null;
+
+    /**
+     * Given an element, find the "key" that should be used to identify it;
+     */
+    getKeyFromElement(element: HTMLElement): string|null;
 
     /**
      * Given an element from a response, find all the events that should be emitted.
      */
-    getEventsToEmit(): Array<{event: string, data: any, target: string|null, componentName: string|null }>;
+    getEventsToEmit(element: HTMLElement): Array<{event: string, data: any, target: string|null, componentName: string|null }>;
 
     /**
      * Given an element from a response, find all the events that should be dispatched.
      */
-    getBrowserEventsToDispatch(): Array<{event: string, payload: any }>;
+    getBrowserEventsToDispatch(element: HTMLElement): Array<{event: string, payload: any }>;
 }
 
-export class StimulusElementDriver implements ElementDriver {
-    private readonly controller: LiveControllerDefault;
-
-    constructor(controller: LiveControllerDefault) {
-        this.controller = controller;
-    }
-
+export class StandardElementDriver implements ElementDriver {
     getModelName(element: HTMLElement): string|null {
         const modelDirective = getModelDirectiveFromElement(element, false);
 
@@ -34,15 +37,29 @@ export class StimulusElementDriver implements ElementDriver {
         return modelDirective.action;
     }
 
-    getComponentProps(): any {
-        return this.controller.propsValue;
+    getComponentProps(rootElement: HTMLElement): any {
+        const propsJson = rootElement.dataset.livePropsValue ?? '{}';
+
+        return JSON.parse(propsJson);
+    }
+
+    findChildComponentElement(id: string, element: HTMLElement): HTMLElement|null {
+        return element.querySelector(`[data-live-id=${id}]`);
     }
 
-    getEventsToEmit(): Array<{event: string, data: any, target: string|null, componentName: string|null }> {
-        return this.controller.eventsToEmitValue;
+    getKeyFromElement(element: HTMLElement): string|null {
+        return element.dataset.liveId || null;
     }
 
-    getBrowserEventsToDispatch(): Array<{event: string, payload: any }> {
-        return this.controller.eventsToDispatchValue;
+    getEventsToEmit(element: HTMLElement): Array<{event: string, data: any, target: string|null, componentName: string|null }> {
+        const eventsJson = element.dataset.liveEmit ?? '[]';
+
+        return JSON.parse(eventsJson);
+    }
+
+    getBrowserEventsToDispatch(element: HTMLElement): Array<{event: string, payload: any }> {
+        const eventsJson = element.dataset.liveBrowserDispatch ?? '[]';
+
+        return JSON.parse(eventsJson);
     }
 }
diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts
index a59ccee9..49026af9 100644
--- a/src/LiveComponent/assets/src/Component/index.ts
+++ b/src/LiveComponent/assets/src/Component/index.ts
@@ -1,4 +1,4 @@
-import { BackendAction, BackendInterface } from '../Backend/Backend';
+import { BackendAction, BackendInterface, ChildrenFingerprints } from '../Backend/Backend';
 import ValueStore from './ValueStore';
 import { normalizeModelName } from '../string_utils';
 import BackendRequest from '../Backend/BackendRequest';
@@ -9,18 +9,31 @@ import { ElementDriver } from './ElementDriver';
 import HookManager from '../HookManager';
 import { PluginInterface } from './plugins/PluginInterface';
 import BackendResponse from '../Backend/BackendResponse';
+import { ModelBinding } from '../Directive/get_model_binding';
 import ExternalMutationTracker from '../Rendering/ExternalMutationTracker';
-import { findComponents, registerComponent, unregisterComponent } from '../ComponentRegistry';
 
 declare const Turbo: any;
 
+export type ComponentFinder = (currentComponent: Component, onlyParents: boolean, onlyMatchName: string|null) => Component[];
+
+class ChildComponentWrapper {
+    component: Component;
+    modelBindings: ModelBinding[];
+
+    constructor(component: Component, modelBindings: ModelBinding[]) {
+        this.component = component;
+        this.modelBindings = modelBindings;
+    }
+}
+
 export default class Component {
     readonly element: HTMLElement;
     readonly name: string;
     // key is the string event name and value is an array of action names
     readonly listeners: Map<string, string[]>;
+    private readonly componentFinder: ComponentFinder;
     private backend: BackendInterface;
-    readonly elementDriver: ElementDriver;
+    private readonly elementDriver: ElementDriver;
     id: string|null;
 
     /**
@@ -30,12 +43,13 @@ export default class Component {
      * to determine if any "input" to the child component changed and thus,
      * if the child component needs to be re-rendered.
      */
-    fingerprint = '';
+    fingerprint: string|null;
 
     readonly valueStore: ValueStore;
     private readonly unsyncedInputsTracker: UnsyncedInputsTracker;
     private hooks: HookManager;
 
+
     defaultDebounce = 150;
 
     private backendRequest: BackendRequest|null = null;
@@ -50,6 +64,9 @@ export default class Component {
     private nextRequestPromise: Promise<BackendResponse>;
     private nextRequestPromiseResolve: (response: BackendResponse) => any;
 
+    private children: Map<string, ChildComponentWrapper> = new Map();
+    private parent: Component|null = null;
+
     private externalMutationTracker: ExternalMutationTracker;
 
     /**
@@ -57,16 +74,20 @@ export default class Component {
      * @param name    The name of the component
      * @param props   Readonly component props
      * @param listeners Array of event -> action listeners
+     * @param componentFinder
+     * @param fingerprint
      * @param id      Some unique id to identify this component. Needed to be a child component
      * @param backend Backend instance for updating
      * @param elementDriver Class to get "model" name from any element.
      */
-    constructor(element: HTMLElement, name: string, props: any, listeners: Array<{ event: string; action: string }>, id: string|null, backend: BackendInterface, elementDriver: ElementDriver) {
+    constructor(element: HTMLElement, name: string, props: any, listeners: Array<{ event: string; action: string }>, componentFinder: ComponentFinder, fingerprint: string|null, id: string|null, backend: BackendInterface, elementDriver: ElementDriver) {
         this.element = element;
         this.name = name;
+        this.componentFinder = componentFinder;
         this.backend = backend;
         this.elementDriver = elementDriver;
         this.id = id;
+        this.fingerprint = fingerprint;
 
         this.listeners = new Map();
         listeners.forEach((listener) => {
@@ -88,6 +109,15 @@ export default class Component {
         // start early to catch any mutations that happen before the component is connected
         // for example, the LoadingPlugin, which sets initial non-loading state
         this.externalMutationTracker.start();
+
+        this.onChildComponentModelUpdate = this.onChildComponentModelUpdate.bind(this);
+    }
+
+    /**
+     * @internal
+     */
+    _swapBackend(backend: BackendInterface) {
+        this.backend = backend;
     }
 
     addPlugin(plugin: PluginInterface) {
@@ -95,14 +125,12 @@ export default class Component {
     }
 
     connect(): void {
-        registerComponent(this);
         this.hooks.triggerHook('connect', this);
         this.unsyncedInputsTracker.activate();
         this.externalMutationTracker.start();
     }
 
     disconnect(): void {
-        unregisterComponent(this);
         this.hooks.triggerHook('disconnect', this);
         this.clearRequestDebounceTimeout();
         this.unsyncedInputsTracker.deactivate();
@@ -114,7 +142,6 @@ export default class Component {
      *
      *     * connect (component: Component) => {}
      *     * disconnect (component: Component) => {}
-     *     * request:started (requestConfig: any) => {}
      *     * render:started (html: string, response: BackendResponse, controls: { shouldRender: boolean }) => {}
      *     * render:finished (component: Component) => {}
      *     * response:error (backendResponse: BackendResponse, controls: { displayError: boolean }) => {}
@@ -192,6 +219,39 @@ export default class Component {
         return this.unsyncedInputsTracker.getUnsyncedModels();
     }
 
+    addChild(child: Component, modelBindings: ModelBinding[] = []): void {
+        if (!child.id) {
+            throw new Error('Children components must have an id.');
+        }
+
+        this.children.set(child.id, new ChildComponentWrapper(child, modelBindings));
+        child.parent = this;
+        child.on('model:set', this.onChildComponentModelUpdate);
+    }
+
+    removeChild(child: Component): void {
+        if (!child.id) {
+            throw new Error('Children components must have an id.');
+        }
+
+        this.children.delete(child.id);
+        child.parent = null;
+        child.off('model:set', this.onChildComponentModelUpdate);
+    }
+
+    getParent(): Component|null {
+        return this.parent;
+    }
+
+    getChildren(): Map<string, Component> {
+        const children: Map<string, Component> = new Map();
+        this.children.forEach((childComponent, id) => {
+            children.set(id, childComponent.component);
+        });
+
+        return children;
+    }
+
     emit(name: string, data: any, onlyMatchingComponentsNamed: string|null = null): void {
         return this.performEmit(name, data, false, onlyMatchingComponentsNamed);
     }
@@ -205,7 +265,7 @@ export default class Component {
     }
 
     private performEmit(name: string, data: any, emitUp: boolean, matchingName: string|null): void {
-        const components: Component[] = findComponents(this, emitUp, matchingName);
+        const components = this.componentFinder(this, emitUp, matchingName);
         components.forEach((component) => {
             component.doEmit(name, data);
         });
@@ -224,6 +284,65 @@ export default class Component {
         });
     }
 
+    /**
+     * Called during morphdom: read props from toEl and re-render if necessary.
+     *
+     * @param toEl
+     */
+    updateFromNewElementFromParentRender(toEl: HTMLElement): boolean {
+        const props = this.elementDriver.getComponentProps(toEl);
+
+        // if no props are on the element, use the existing element completely
+        // this means the parent is signaling that the child does not need to be re-rendered
+        if (props === null) {
+            return false;
+        }
+
+        // push props directly down onto the value store
+        const isChanged = this.valueStore.storeNewPropsFromParent(props);
+
+        const fingerprint = toEl.dataset.liveFingerprintValue;
+        if (fingerprint !== undefined) {
+            this.fingerprint = fingerprint;
+        }
+
+        if (isChanged) {
+            this.render();
+        }
+
+        return isChanged;
+    }
+
+    /**
+     * Handles data-model binding from a parent component onto a child.
+     */
+    onChildComponentModelUpdate(modelName: string, value: any, childComponent: Component): void {
+        if (!childComponent.id) {
+            throw new Error('Missing id');
+        }
+
+        const childWrapper = this.children.get(childComponent.id);
+        if (!childWrapper) {
+            throw new Error('Missing child');
+        }
+
+        childWrapper.modelBindings.forEach((modelBinding) => {
+            const childModelName = modelBinding.innerModelName || 'value';
+
+            // skip, unless childModelName matches the model that just changed
+            if (childModelName !== modelName) {
+                return;
+            }
+
+            this.set(
+                modelBinding.modelName,
+                value,
+                modelBinding.shouldRender,
+                modelBinding.debounce
+            );
+        });
+    }
+
     private isTurboEnabled(): boolean {
         return typeof Turbo !== 'undefined' && !this.element.closest('[data-turbo="false"]');
     }
@@ -256,22 +375,13 @@ export default class Component {
             }
         }
 
-        const requestConfig = {
-            props: this.valueStore.getOriginalProps(),
-            actions: this.pendingActions,
-            updated: this.valueStore.getDirtyProps(),
-            children: {},
-            updatedPropsFromParent: this.valueStore.getUpdatedPropsFromParent(),
-            files: filesToSend,
-        };
-        this.hooks.triggerHook('request:started', requestConfig);
         this.backendRequest = this.backend.makeRequest(
-            requestConfig.props,
-            requestConfig.actions,
-            requestConfig.updated,
-            requestConfig.children,
-            requestConfig.updatedPropsFromParent,
-            requestConfig.files
+            this.valueStore.getOriginalProps(),
+            this.pendingActions,
+            this.valueStore.getDirtyProps(),
+            this.getChildrenFingerprints(),
+            this.valueStore.getUpdatedPropsFromParent(),
+            filesToSend,
         );
         this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
 
@@ -368,6 +478,12 @@ export default class Component {
             throw error;
         }
 
+        const newProps = this.elementDriver.getComponentProps(newElement);
+        this.valueStore.reinitializeAllProps(newProps);
+
+        const eventsToEmit = this.elementDriver.getEventsToEmit(newElement);
+        const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch(newElement);
+
         // make sure we've processed all external changes before morphing
         this.externalMutationTracker.handlePendingChanges();
         this.externalMutationTracker.stop();
@@ -376,16 +492,13 @@ export default class Component {
             newElement,
             this.unsyncedInputsTracker.getUnsyncedInputs(),
             (element: HTMLElement) => getValueFromElement(element, this.valueStore),
+            Array.from(this.getChildren().values()),
+            this.elementDriver.findChildComponentElement,
+            this.elementDriver.getKeyFromElement,
             this.externalMutationTracker
         );
         this.externalMutationTracker.start();
 
-        const newProps = this.elementDriver.getComponentProps();
-        this.valueStore.reinitializeAllProps(newProps);
-
-        const eventsToEmit = this.elementDriver.getEventsToEmit();
-        const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch();
-
         // reset the modified values back to their client-side version
         Object.keys(modifiedModelValues).forEach((modelName) => {
             this.valueStore.set(modelName, modifiedModelValues[modelName]);
@@ -496,24 +609,30 @@ export default class Component {
         modal.focus();
     }
 
+    private getChildrenFingerprints(): ChildrenFingerprints {
+        const fingerprints: ChildrenFingerprints = {};
+
+        this.children.forEach((childComponent) => {
+            const child = childComponent.component;
+            if (!child.id) {
+                throw new Error('missing id');
+            }
+
+            fingerprints[child.id] = {
+                fingerprint: child.fingerprint as string,
+                tag: child.element.tagName.toLowerCase(),
+            };
+        });
+
+        return fingerprints;
+    }
+
     private resetPromise(): void {
         this.nextRequestPromise = new Promise((resolve) => {
             this.nextRequestPromiseResolve = resolve;
         });
     }
 
-    /**
-     * Called on a child component after the parent component render has requested
-     * that the child component update its props & re-render if necessary.
-     */
-    _updateFromParentProps(props: any) {
-        // push props directly down onto the value store
-        const isChanged = this.valueStore.storeNewPropsFromParent(props);
-
-        if (isChanged) {
-            this.render();
-        }
-    }
 }
 
 /**
diff --git a/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts
deleted file mode 100644
index 1ad3e9c1..00000000
--- a/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import Component from '../../Component';
-import { PluginInterface } from './PluginInterface';
-import { ChildrenFingerprints } from '../../Backend/Backend';
-import getModelBinding, { ModelBinding } from '../../Directive/get_model_binding';
-import { getAllModelDirectiveFromElements } from '../../dom_utils';
-import { findChildren, findParent } from '../../ComponentRegistry';
-
-/**
- * Handles all interactions for child components of a component.
- *
- *      A) This parent component handling its children:
- *      * Sending children fingerprints to the server.
- *
- *      B) This child component handling its parent:
- *      * Notifying the parent of a model change.
- */
-export default class implements PluginInterface {
-    private readonly component: Component;
-    private parentModelBindings: ModelBinding[] = [];
-
-    constructor(component: Component) {
-        this.component  = component;
-
-        const modelDirectives = getAllModelDirectiveFromElements(this.component.element);
-        this.parentModelBindings = modelDirectives.map(getModelBinding);
-    }
-
-    attachToComponent(component: Component): void {
-        component.on('request:started', (requestData: any) => {
-            requestData.children = this.getChildrenFingerprints();
-        });
-
-        component.on('model:set', (model: string, value: any) => {
-            this.notifyParentModelChange(model, value);
-        });
-    }
-
-    private getChildrenFingerprints(): ChildrenFingerprints {
-        const fingerprints: ChildrenFingerprints = {};
-
-        this.getChildren().forEach((child) => {
-            if (!child.id) {
-                throw new Error('missing id');
-            }
-
-            fingerprints[child.id] = {
-                fingerprint: child.fingerprint as string,
-                tag: child.element.tagName.toLowerCase(),
-            };
-        });
-
-        return fingerprints;
-    }
-
-    /**
-     * Notifies parent of a model change if desired.
-     *
-     * This makes the child "behave" like it's a normal `<input>` element,
-     * where, when its value changes, the parent is notified.
-     */
-    private notifyParentModelChange(modelName: string, value: any): void {
-        const parentComponent = findParent(this.component);
-
-        if (!parentComponent) {
-            return;
-        }
-
-        this.parentModelBindings.forEach((modelBinding) => {
-            const childModelName = modelBinding.innerModelName || 'value';
-
-            // skip, unless childModelName matches the model that just changed
-            if (childModelName !== modelName) {
-                return;
-            }
-
-            parentComponent.set(
-                modelBinding.modelName,
-                value,
-                modelBinding.shouldRender,
-                modelBinding.debounce
-            );
-        });
-    }
-
-    private getChildren(): Component[] {
-        return findChildren(this.component);
-    }
-}
diff --git a/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts
index ef5b7c71..f2e307c7 100644
--- a/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts
+++ b/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts
@@ -151,7 +151,7 @@ export default class implements PluginInterface {
 
     getLoadingDirectives(component: Component, element: HTMLElement|SVGElement) {
         const loadingDirectives: ElementLoadingDirectives[] = [];
-        let matchingElements = [...Array.from(element.querySelectorAll('[data-loading]'))];
+        let matchingElements = [...element.querySelectorAll('[data-loading]')];
 
         // ignore elements which are inside a nested "live" component
         matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
diff --git a/src/LiveComponent/assets/src/ComponentRegistry.ts b/src/LiveComponent/assets/src/ComponentRegistry.ts
index 3332d823..752d6394 100644
--- a/src/LiveComponent/assets/src/ComponentRegistry.ts
+++ b/src/LiveComponent/assets/src/ComponentRegistry.ts
@@ -1,119 +1,63 @@
 import Component from './Component';
-import getElementAsTagText from './Util/getElementAsTagText';
-
-let componentMapByElement = new WeakMap<HTMLElement, Component>();
-/**
- * The value is the component's name.
- */
-let componentMapByComponent = new Map<Component, string>();
-
-export const resetRegistry = function () {
-    componentMapByElement = new WeakMap<HTMLElement, Component>();
-    componentMapByComponent = new Map<Component, string>();
-};
-
-export const registerComponent = function (component: Component) {
-    componentMapByElement.set(component.element, component);
-    componentMapByComponent.set(component, component.name);
-};
-
-export const unregisterComponent = function (component: Component) {
-    componentMapByElement.delete(component.element);
-    componentMapByComponent.delete(component);
-};
-
-export const getComponent = function (element: HTMLElement): Promise<Component> {
-    return new Promise((resolve, reject) => {
-        let count = 0;
-        const maxCount = 10;
-        const interval = setInterval(() => {
-            const component = componentMapByElement.get(element);
-            if (component) {
-                clearInterval(interval);
-                resolve(component);
-            }
-            count++;
-
-            if (count > maxCount) {
-                clearInterval(interval);
-                reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
-            }
-        }, 5);
-    });
-};
-
-/**
- * Returns a filtered list of all the currently-registered components
- */
-export const findComponents = function (
-    currentComponent: Component,
-    onlyParents: boolean,
-    onlyMatchName: string | null
-): Component[] {
-    const components: Component[] = [];
-    componentMapByComponent.forEach((componentName: string, component: Component) => {
-        if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) {
-            return;
-        }
-
-        if (onlyMatchName && componentName !== onlyMatchName) {
-            return;
-        }
-
-        components.push(component);
-    });
-
-    return components;
-};
+import { getElementAsTagText } from './dom_utils';
+
+export default class {
+    private componentMapByElement = new WeakMap<HTMLElement, Component>();
+    /**
+     * The value is the component's name.
+     */
+    private componentMapByComponent = new Map<Component, string>();
+
+    public registerComponent(element: HTMLElement, component: Component) {
+        this.componentMapByElement.set(element, component);
+        this.componentMapByComponent.set(component, component.name);
+    }
 
-/**
- * Returns an array of components that are direct children of the given component.
- */
-export const findChildren = function (currentComponent: Component): Component[] {
-    const children: Component[] = [];
-    componentMapByComponent.forEach((componentName: string, component: Component) => {
-        if (currentComponent === component) {
-            return;
-        }
+    public unregisterComponent(component: Component) {
+        this.componentMapByElement.delete(component.element);
+        this.componentMapByComponent.delete(component);
+    }
 
-        if (!currentComponent.element.contains(component.element)) {
-            return;
-        }
+    public getComponent(element: HTMLElement): Promise<Component> {
+        return new Promise((resolve, reject) => {
+            let count = 0;
+            const maxCount = 10;
+            const interval = setInterval(() => {
+                const component = this.componentMapByElement.get(element);
+                if (component) {
+                    clearInterval(interval);
+                    resolve(component);
+                }
+                count++;
+
+                if (count > maxCount) {
+                    clearInterval(interval);
+                    reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
+                }
+            }, 5);
+        });
+    }
 
-        // check if there are any other components between the two
-        let foundChildComponent = false;
-        componentMapByComponent.forEach((childComponentName: string, childComponent: Component) => {
-            if (foundChildComponent) {
-                // return early
+    /**
+     * Returns a filtered list of all the currently-registered components
+     */
+    findComponents(currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null): Component[] {
+        const components: Component[] = [];
+        this.componentMapByComponent.forEach((componentName: string, component: Component) => {
+            if (
+                onlyParents &&
+                (currentComponent === component || !component.element.contains(currentComponent.element))
+            ) {
                 return;
             }
 
-            if (childComponent === component) {
+            if (onlyMatchName && componentName !== onlyMatchName) {
                 return;
             }
 
-            if (childComponent.element.contains(component.element)) {
-                foundChildComponent = true;
-            }
+            components.push(component);
         });
 
-        children.push(component);
-    });
-
-    return children;
-};
-
-export const findParent = function (currentComponent: Component): Component | null {
-    // recursively traverse the node tree up to find a parent
-    let parentElement = currentComponent.element.parentElement;
-    while (parentElement) {
-        const component = componentMapByElement.get(parentElement);
-        if (component) {
-            return component;
-        }
-
-        parentElement = parentElement.parentElement;
+        return components;
     }
-
-    return null;
-};
+}
diff --git a/src/LiveComponent/assets/src/Util/getElementAsTagText.ts b/src/LiveComponent/assets/src/Util/getElementAsTagText.ts
deleted file mode 100644
index 42e10daa..00000000
--- a/src/LiveComponent/assets/src/Util/getElementAsTagText.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Returns just the outer element's HTML as a string - useful for error messages.
- *
- * For example:
- *      <div class="outer">And text inside <p>more text</p></div>
- *
- * Would return:
- *      <div class="outer">
- */
-export default function getElementAsTagText(element: HTMLElement): string {
-    return element.innerHTML
-        ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
-        : element.outerHTML;
-}
diff --git a/src/LiveComponent/assets/src/dom_utils.ts b/src/LiveComponent/assets/src/dom_utils.ts
index b756f3de..310692e3 100644
--- a/src/LiveComponent/assets/src/dom_utils.ts
+++ b/src/LiveComponent/assets/src/dom_utils.ts
@@ -2,8 +2,6 @@ import ValueStore from './Component/ValueStore';
 import { Directive, parseDirectives } from './Directive/directives_parser';
 import { normalizeModelName } from './string_utils';
 import Component from './Component';
-import { findChildren } from './ComponentRegistry';
-import getElementAsTagText from './Util/getElementAsTagText';
 
 /**
  * Return the "value" of any given element.
@@ -208,7 +206,7 @@ export function elementBelongsToThisComponent(element: Element, component: Compo
     }
 
     let foundChildComponent = false;
-    findChildren(component).forEach((childComponent) => {
+    component.getChildren().forEach((childComponent) => {
         if (foundChildComponent) {
             // return early
             return;
@@ -256,6 +254,21 @@ export function htmlToElement(html: string): HTMLElement {
     return child;
 }
 
+/**
+ * Returns just the outer element's HTML as a string - useful for error messages.
+ *
+ * For example:
+ *      <div class="outer">And text inside <p>more text</p></div>
+ *
+ * Would return:
+ *      <div class="outer">
+ */
+export function getElementAsTagText(element: HTMLElement): string {
+    return element.innerHTML
+        ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
+        : element.outerHTML;
+}
+
 const getMultipleCheckboxValue = function (element: HTMLInputElement, currentValues: Array<string>): Array<string> {
     const finalValues = [...currentValues];
     const value = inputValue(element);
diff --git a/src/LiveComponent/assets/src/live_controller.ts b/src/LiveComponent/assets/src/live_controller.ts
index 25025c55..86c4bf96 100644
--- a/src/LiveComponent/assets/src/live_controller.ts
+++ b/src/LiveComponent/assets/src/live_controller.ts
@@ -1,9 +1,15 @@
 import { Controller } from '@hotwired/stimulus';
 import { parseDirectives, DirectiveModifier } from './Directive/directives_parser';
-import { getModelDirectiveFromElement, getValueFromElement, elementBelongsToThisComponent } from './dom_utils';
+import {
+    getModelDirectiveFromElement,
+    getElementAsTagText,
+    getValueFromElement,
+    elementBelongsToThisComponent,
+    getAllModelDirectiveFromElements,
+} from './dom_utils';
 import Component, { proxifyComponent } from './Component';
-import Backend, { BackendInterface } from './Backend/Backend';
-import { StimulusElementDriver } from './Component/ElementDriver';
+import Backend from './Backend/Backend';
+import { StandardElementDriver } from './Component/ElementDriver';
 import LoadingPlugin from './Component/plugins/LoadingPlugin';
 import ValidatedFieldsPlugin from './Component/plugins/ValidatedFieldsPlugin';
 import PageUnloadingPlugin from './Component/plugins/PageUnloadingPlugin';
@@ -11,12 +17,12 @@ import PollingPlugin from './Component/plugins/PollingPlugin';
 import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModelFieldsPlugin';
 import { PluginInterface } from './Component/plugins/PluginInterface';
 import getModelBinding from './Directive/get_model_binding';
+import ComponentRegistry from './ComponentRegistry';
 import QueryStringPlugin from './Component/plugins/QueryStringPlugin';
-import ChildComponentPlugin from './Component/plugins/ChildComponentPlugin';
-import getElementAsTagText from './Util/getElementAsTagText';
 
 export { Component };
-export { getComponent } from './ComponentRegistry';
+export const getComponent = (element: HTMLElement): Promise<Component> =>
+    LiveControllerDefault.componentRegistry.getComponent(element);
 
 export interface LiveEvent extends CustomEvent {
     detail: {
@@ -33,13 +39,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     static values = {
         name: String,
         url: String,
-        props: { type: Object, default: {} },
-        propsUpdatedFromParent: { type: Object, default: {} },
+        props: Object,
         csrf: String,
         listeners: { type: Array, default: [] },
-        eventsToEmit: { type: Array, default: [] },
-        eventsToDispatch: { type: Array, default: [] },
         debounce: { type: Number, default: 150 },
+        id: String,
         fingerprint: { type: String, default: '' },
         requestMethod: { type: String, default: 'post' },
         queryMapping: { type: Object, default: {} },
@@ -48,16 +52,8 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     declare readonly nameValue: string;
     declare readonly urlValue: string;
     declare readonly propsValue: any;
-    declare propsUpdatedFromParentValue: any;
     declare readonly csrfValue: string;
     declare readonly listenersValue: Array<{ event: string; action: string }>;
-    declare readonly eventsToEmitValue: Array<{
-        event: string;
-        data: any;
-        target: string | null;
-        componentName: string | null;
-    }>;
-    declare readonly eventsToDispatchValue: Array<{ event: string; payload: any }>;
     declare readonly hasDebounceValue: boolean;
     declare readonly debounceValue: number;
     declare readonly fingerprintValue: string;
@@ -66,7 +62,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
 
     /** The component, wrapped in the convenience Proxy */
     private proxiedComponent: Component;
-    private mutationObserver: MutationObserver;
     /** The raw Component object */
     component: Component;
     pendingActionTriggerModelElement: HTMLElement | null = null;
@@ -74,30 +69,71 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
     private elementEventListeners: Array<{ event: string; callback: (event: any) => void }> = [
         { event: 'input', callback: (event) => this.handleInputEvent(event) },
         { event: 'change', callback: (event) => this.handleChangeEvent(event) },
+        { event: 'live:connect', callback: (event) => this.handleConnectedControllerEvent(event) },
     ];
     private pendingFiles: { [key: string]: HTMLInputElement } = {};
 
-    static backendFactory: (controller: LiveControllerDefault) => BackendInterface = (controller) =>
-        new Backend(controller.urlValue, controller.requestMethodValue, controller.csrfValue);
+    static componentRegistry = new ComponentRegistry();
 
     initialize() {
-        this.mutationObserver = new MutationObserver(this.onMutations.bind(this));
+        this.handleDisconnectedChildControllerEvent = this.handleDisconnectedChildControllerEvent.bind(this);
 
-        this.createComponent();
+        const id = this.element.dataset.liveId || null;
+
+        this.component = new Component(
+            this.element,
+            this.nameValue,
+            this.propsValue,
+            this.listenersValue,
+            (currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null) =>
+                LiveControllerDefault.componentRegistry.findComponents(currentComponent, onlyParents, onlyMatchName),
+            this.fingerprintValue,
+            id,
+            new Backend(this.urlValue, this.requestMethodValue, this.csrfValue),
+            new StandardElementDriver()
+        );
+        this.proxiedComponent = proxifyComponent(this.component);
+
+        // @ts-ignore Adding the dynamic property
+        this.element.__component = this.proxiedComponent;
+
+        if (this.hasDebounceValue) {
+            this.component.defaultDebounce = this.debounceValue;
+        }
+
+        const plugins: PluginInterface[] = [
+            new LoadingPlugin(),
+            new ValidatedFieldsPlugin(),
+            new PageUnloadingPlugin(),
+            new PollingPlugin(),
+            new SetValueOntoModelFieldsPlugin(),
+            new QueryStringPlugin(this.queryMappingValue),
+        ];
+        plugins.forEach((plugin) => {
+            this.component.addPlugin(plugin);
+        });
     }
 
     connect() {
-        this.connectComponent();
+        LiveControllerDefault.componentRegistry.registerComponent(this.element, this.component);
+        this.component.connect();
 
-        this.mutationObserver.observe(this.element, {
-            attributes: true,
+        this.elementEventListeners.forEach(({ event, callback }) => {
+            this.component.element.addEventListener(event, callback);
         });
+
+        this.dispatchEvent('connect');
     }
 
-    disconnect(): void {
-        this.disconnectComponent();
+    disconnect() {
+        LiveControllerDefault.componentRegistry.unregisterComponent(this.component);
+        this.component.disconnect();
 
-        this.mutationObserver.disconnect();
+        this.elementEventListeners.forEach(({ event, callback }) => {
+            this.component.element.removeEventListener(event, callback);
+        });
+
+        this.dispatchEvent('disconnect');
     }
 
     /**
@@ -188,6 +224,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         });
     }
 
+    $render() {
+        return this.component.render();
+    }
+
     emit(event: Event) {
         this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
             this.component.emit(name, data, nameMatch);
@@ -206,30 +246,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         });
     }
 
-    $render() {
-        return this.component.render();
-    }
-
-    /**
-     * Update a model value.
-     *
-     * @param {string} model The model to update
-     * @param {any} value The new value
-     * @param {boolean} shouldRender Whether a re-render should be triggered
-     * @param {number|boolean} debounce
-     */
-    $updateModel(model: string, value: any, shouldRender = true, debounce: number | boolean = true) {
-        return this.component.set(model, value, shouldRender, debounce);
-    }
-
-    propsUpdatedFromParentValueChanged() {
-        this.component._updateFromParentProps(this.propsUpdatedFromParentValue);
-    }
-
-    fingerprintValueChanged() {
-        this.component.fingerprint = this.fingerprintValue;
-    }
-
     private getEmitDirectives(event: Event): Array<{ name: string; data: any; nameMatch: string | null }> {
         const element = event.currentTarget as HTMLElement;
         if (!element.dataset.event) {
@@ -263,60 +279,24 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         return emits;
     }
 
-    private createComponent(): void {
-        const id = this.element.id || null;
-
-        this.component = new Component(
-            this.element,
-            this.nameValue,
-            this.propsValue,
-            this.listenersValue,
-            id,
-            LiveControllerDefault.backendFactory(this),
-            new StimulusElementDriver(this)
-        );
-        this.proxiedComponent = proxifyComponent(this.component);
-
-        // @ts-ignore Adding the dynamic property
-        this.element.__component = this.proxiedComponent;
-
-        if (this.hasDebounceValue) {
-            this.component.defaultDebounce = this.debounceValue;
-        }
-
-        const plugins: PluginInterface[] = [
-            new LoadingPlugin(),
-            new ValidatedFieldsPlugin(),
-            new PageUnloadingPlugin(),
-            new PollingPlugin(),
-            new SetValueOntoModelFieldsPlugin(),
-            new QueryStringPlugin(this.queryMappingValue),
-            new ChildComponentPlugin(this.component),
-        ];
-        plugins.forEach((plugin) => {
-            this.component.addPlugin(plugin);
-        });
-    }
-
-    private connectComponent() {
-        this.component.connect();
-        this.mutationObserver.observe(this.element, {
-            attributes: true,
-        });
-
-        this.elementEventListeners.forEach(({ event, callback }) => {
-            this.component.element.addEventListener(event, callback);
-        });
-
-        this.dispatchEvent('connect');
-    }
-
-    private disconnectComponent() {
-        this.component.disconnect();
-        this.elementEventListeners.forEach(({ event, callback }) => {
-            this.component.element.removeEventListener(event, callback);
-        });
-        this.dispatchEvent('disconnect');
+    /**
+     * Update a model value.
+     *
+     * The extraModelName should be set to the "name" attribute of an element
+     * if it has one. This is only important in a parent/child component,
+     * where, in the child, you might be updating a "foo" model, but you
+     * also want this update to "sync" to the parent component's "bar" model.
+     * Typically, setup on a field like this:
+     *
+     *      <input data-model="foo" name="bar">
+     *
+     * @param {string} model The model to update
+     * @param {any} value The new value
+     * @param {boolean} shouldRender Whether a re-render should be triggered
+     * @param {number|boolean} debounce
+     */
+    $updateModel(model: string, value: any, shouldRender = true, debounce: number | boolean = true) {
+        return this.component.set(model, value, shouldRender, debounce);
     }
 
     private handleInputEvent(event: Event) {
@@ -421,24 +401,48 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         this.component.set(modelBinding.modelName, finalValue, modelBinding.shouldRender, modelBinding.debounce);
     }
 
+    handleConnectedControllerEvent(event: LiveEvent): void {
+        if (event.target === this.element) {
+            return;
+        }
+
+        const childController = event.detail.controller;
+        if (childController.component.getParent()) {
+            // child already has a parent - we are a grandparent
+            return;
+        }
+
+        const modelDirectives = getAllModelDirectiveFromElements(childController.element);
+        const modelBindings = modelDirectives.map(getModelBinding);
+
+        this.component.addChild(childController.component, modelBindings);
+
+        // live:disconnect needs to be registered on the child element directly
+        // that's because if the child component is removed from the DOM, then
+        // the parent controller is no longer an ancestor, so the live:disconnect
+        // event would not bubble up to it.
+        // @ts-ignore TS doesn't like the LiveEvent arg in the listener, not sure how to fix
+        childController.element.addEventListener('live:disconnect', this.handleDisconnectedChildControllerEvent);
+    }
+
+    handleDisconnectedChildControllerEvent(event: LiveEvent): void {
+        const childController = event.detail.controller;
+
+        // @ts-ignore TS doesn't like the LiveEvent arg in the listener, not sure how to fix
+        childController.element.removeEventListener('live:disconnect', this.handleDisconnectedChildControllerEvent);
+
+        // this shouldn't happen: but double-check we're the parent
+        if (childController.component.getParent() !== this.component) {
+            return;
+        }
+
+        this.component.removeChild(childController.component);
+    }
+
     private dispatchEvent(name: string, detail: any = {}, canBubble = true, cancelable = false) {
         detail.controller = this;
         detail.component = this.proxiedComponent;
 
         this.dispatch(name, { detail, prefix: 'live', cancelable, bubbles: canBubble });
     }
-
-    private onMutations(mutations: MutationRecord[]): void {
-        mutations.forEach((mutation) => {
-            if (
-                mutation.type === 'attributes' &&
-                mutation.attributeName === 'id' &&
-                this.element.id !== this.component.id
-            ) {
-                this.disconnectComponent();
-                this.createComponent();
-                this.connectComponent();
-            }
-        });
-    }
 }
diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts
index 7dba0668..52a45536 100644
--- a/src/LiveComponent/assets/src/morphdom.ts
+++ b/src/LiveComponent/assets/src/morphdom.ts
@@ -2,54 +2,22 @@ import { cloneHTMLElement, setValueOnElement } from './dom_utils';
 // @ts-ignore
 import { Idiomorph } from 'idiomorph/dist/idiomorph.esm.js';
 import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison';
+import Component from './Component';
 import ExternalMutationTracker from './Rendering/ExternalMutationTracker';
 
-const syncAttributes = function (fromEl: Element, toEl: Element): void {
-    for (let i = 0; i < fromEl.attributes.length; i++) {
-        const attr = fromEl.attributes[i];
-        toEl.setAttribute(attr.name, attr.value);
-    }
-};
-
 export function executeMorphdom(
     rootFromElement: HTMLElement,
     rootToElement: HTMLElement,
     modifiedFieldElements: Array<HTMLElement>,
     getElementValue: (element: HTMLElement) => any,
+    childComponents: Component[],
+    findChildComponent: (id: string, element: HTMLElement) => HTMLElement | null,
+    getKeyFromElement: (element: HTMLElement) => string | null,
     externalMutationTracker: ExternalMutationTracker
 ) {
-    /*
-     * Handle "data-live-preserve" elements.
-     *
-     * These are elements that are empty and have requested that their
-     * content be preserved from the matching element of the existing HTML.
-     *
-     * To handle them, we:
-     *  1) Create an array of the "current" HTMLElements that match each
-     *     "data-live-preserve" element.
-     *  2) Replace the "current" elements with clones so that the originals
-     *     aren't modified during the morphing process.
-     *  3) After the morphing is complete, we find the preserved elements and
-     *     replace them with the originals.
-     */
-    const preservedOriginalElements: HTMLElement[] = [];
-    rootToElement.querySelectorAll('[data-live-preserve]').forEach((newElement) => {
-        const id = newElement.id;
-        if (!id) {
-            throw new Error('The data-live-preserve attribute requires an id attribute to be set on the element');
-        }
-
-        const oldElement = rootFromElement.querySelector(`#${id}`);
-        if (!(oldElement instanceof HTMLElement)) {
-            throw new Error(`The element with id "${id}" was not found in the original HTML`);
-        }
-
-        const clonedOldElement = cloneHTMLElement(oldElement);
-        preservedOriginalElements.push(oldElement);
-        oldElement.replaceWith(clonedOldElement);
-
-        newElement.removeAttribute('data-live-preserve');
-        syncAttributes(newElement, oldElement);
+    const childComponentMap: Map<HTMLElement, Component> = new Map();
+    childComponents.forEach((childComponent) => {
+        childComponentMap.set(childComponent.element, childComponent);
     });
 
     Idiomorph.morph(rootFromElement, rootToElement, {
@@ -64,31 +32,44 @@ export function executeMorphdom(
                     return true;
                 }
 
+                let idChanged = false;
+                // Track children if data-live-id changed
+                if (fromEl.hasAttribute('data-live-id')) {
+                    if (fromEl.getAttribute('data-live-id') !== toEl.getAttribute('data-live-id')) {
+                        for (const child of fromEl.children) {
+                            child.setAttribute('parent-live-id-changed', '');
+                        }
+                        idChanged = true;
+                    }
+                }
+
                 // skip special checking if this is, for example, an SVG
                 if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
                     // We assume fromEl is an Alpine component if it has `__x` property.
                     // If it's the case, then we should morph `fromEl` to `ToEl` (thanks to https://alpinejs.dev/plugins/morph)
                     // in order to keep the component state and UI in sync.
-                    // @ts-ignore
                     if (typeof fromEl.__x !== 'undefined') {
-                        // @ts-ignore
                         if (!window.Alpine) {
                             throw new Error(
                                 'Unable to access Alpine.js though the global window.Alpine variable. Please make sure Alpine.js is loaded before Symfony UX LiveComponent.'
                             );
                         }
 
-                        // @ts-ignore
                         if (typeof window.Alpine.morph !== 'function') {
                             throw new Error(
                                 'Unable to access Alpine.js morph function. Please make sure the Alpine.js Morph plugin is installed and loaded, see https://alpinejs.dev/plugins/morph for more information.'
                             );
                         }
 
-                        // @ts-ignore
                         window.Alpine.morph(fromEl.__x, toEl);
                     }
 
+                    if (childComponentMap.has(fromEl)) {
+                        const childComponent = childComponentMap.get(fromEl) as Component;
+
+                        return !childComponent.updateFromNewElementFromParentRender(toEl) && idChanged;
+                    }
+
                     if (externalMutationTracker.wasElementAdded(fromEl)) {
                         fromEl.insertAdjacentElement('afterend', toEl);
                         return false;
@@ -125,17 +106,19 @@ export function executeMorphdom(
                     }
                 }
 
-                // data-skip-morph implies you want this element's innerHTML to be
-                // replaced, not morphed. We add the same behavior to elements where
-                // the id has changed. So, even if a <div id="foo"> appears on the
-                // same place as a <div id="bar">, we replace the content to get
-                // totally fresh internals.
-                if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
+                // Update child if parent has his live-id changed
+                if (fromEl.hasAttribute('parent-live-id-changed')) {
+                    fromEl.removeAttribute('parent-live-id-changed');
+
+                    return true;
+                }
+
+                if (fromEl.hasAttribute('data-skip-morph')) {
                     fromEl.innerHTML = toEl.innerHTML;
 
                     return true;
                 }
-                // if parent's innerHTML was replaced, skip morphing on child
+
                 if (fromEl.parentElement && fromEl.parentElement.hasAttribute('data-skip-morph')) {
                     return false;
                 }
@@ -160,14 +143,13 @@ export function executeMorphdom(
         },
     });
 
-    preservedOriginalElements.forEach((oldElement) => {
-        const newElement = rootFromElement.querySelector(`#${oldElement.id}`);
-        if (!(newElement instanceof HTMLElement)) {
-            // should not happen, as preservedOriginalElements is built from
-            // the new HTML
-            throw new Error('Missing preserved element');
+    childComponentMap.forEach((childComponent, element) => {
+        const childComponentInResult = findChildComponent(childComponent.id ?? '', rootFromElement);
+        if (null === childComponentInResult || element === childComponentInResult) {
+            return;
         }
 
-        newElement.replaceWith(oldElement);
+        childComponentInResult?.replaceWith(element);
+        childComponent.updateFromNewElementFromParentRender(childComponentInResult);
     });
 }
diff --git a/src/LiveComponent/composer.json b/src/LiveComponent/composer.json
index dcb507d3..83b9f2af 100644
--- a/src/LiveComponent/composer.json
+++ b/src/LiveComponent/composer.json
@@ -29,7 +29,7 @@
         "php": ">=8.1",
         "symfony/property-access": "^5.4.5|^6.0|^7.0",
         "symfony/ux-twig-component": "^2.8",
-        "twig/twig": "^2.14.7|^3.0.4"
+        "twig/twig": "^2.14.7|~3.8.0"
     },
     "require-dev": {
         "doctrine/annotations": "^1.0",
diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst
index 36a0264b..ad9bb97f 100644
--- a/src/LiveComponent/doc/index.rst
+++ b/src/LiveComponent/doc/index.rst
@@ -2738,7 +2738,7 @@ current value for all props, except for those that are marked as
 
 What if you *do* want your entire child component to re-render (including
 resetting writable live props) when some value in the parent changes? This
-can be done by manually giving your component an ``id`` attribute
+can be done by manually giving your component a ``data-live-id`` attribute
 that will change if the component should be totally re-rendered:
 
 .. code-block:: html+twig
@@ -2749,11 +2749,11 @@ that will change if the component should be totally re-rendered:
 
         {{ component('TodoFooter', {
             count: todos|length,
-            id: 'todo-footer-'~todos|length
+            'data-live-id': 'todo-footer-'~todos|length
         }) }}
     </div>
 
-In this case, if the number of todos change, then the ``id``
+In this case, if the number of todos change, then the ``data-live-id``
 attribute of the component will also change. This signals that the
 component should re-render itself completely, discarding any writable
 LiveProp values.
@@ -2959,14 +2959,14 @@ Rendering Quirks with List of Elements
 
 If you're rendering a list of elements in your component, to help LiveComponents
 understand which element is which between re-renders (i.e. if something re-orders
-or removes some of those elements), you can add a ``id`` attribute to
+or removes some of those elements), you can add a ``data-live-id`` attribute to
 each element
 
 .. code-block:: html+twig
 
     {# templates/components/Invoice.html.twig #}
     {% for lineItem in lineItems %}
-        <div id="{{ lineItem.id }}">
+        <div data-live-id="{{ lineItem.id }}">
             {{ lineItem.name }}
         </div>
     {% endfor %}
@@ -2998,9 +2998,9 @@ to that component:
         }) }}
     {% endfor %}
 
-The ``key`` will be used to generate an ``id`` attribute,
+The ``key`` will be used to generate a ``data-live-id`` attribute,
 which will be used to identify each child component. You can
-also pass in a ``id`` attribute directly, but ``key`` is
+also pass in a ``data-live-id`` attribute directly, but ``key`` is
 a bit more convenient.
 
 .. _rendering-loop-new-element:
@@ -3190,23 +3190,39 @@ The system doesn't handle every edge case, so here are some things to keep in mi
   that change is **lost**: the element will be re-added in its original location
   during the next re-render.
 
-The Mystical id Attribute
-~~~~~~~~~~~~~~~~~~~~~~~~~
+The Mystical data-live-id Attribute
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The ``id`` attribute is mentioned several times throughout the documentation
+The ``data-live-id`` attribute is mentioned several times throughout the documentation
 to solve various problems. It's usually not needed, but can be the key to solving
 certain complex problems. But what is it?
 
 .. note::
 
-    The :ref:`key prop <key-prop>` is used to create a ``id`` attribute
+    The :ref:`key prop <key-prop>` is used to create a ``data-live-id`` attribute
     on child components. So everything in this section applies equally to the
     ``key`` prop.
 
-The ``id`` attribute is a unique identifier for an element or a component.
-It's used during the morphing process when a component re-renders: it helps the
-`morphing library`_ "connect" elements or components in the existing HTML with the new
-HTML.
+The ``data-live-id`` attribute is a unique identifier for an element or a component.
+It's used when a component re-renders and helps Live Components "connect" elements
+or components in the existing HTML with the new HTML. The logic works like this:
+
+Suppose an element or component in the new HTML has a ``data-live-id="some-id"`` attribute.
+Then:
+
+A) If there **is** an element or component with ``data-live-id="some-id"`` in the
+   existing HTML, then the old and new elements/components are considered to be the
+   "same". For elements, the new element will be used to update the old element even
+   if the two elements appear in different places - e.g. like if :ref:`elements are moved <rendering-loop-of-elements>`
+   or re-ordered. For components, because child components render independently
+   from their parent, the existing component will be "left alone" and not re-rendered
+   (unless some ``updateFromParent`` props have changed - see :ref:`child-component-independent-rerender`).
+
+B) If there is **not** an element or component with ``data-live-id="some-id"`` in
+   the existing HTML, then the new element or component is considered to be "new".
+   In both cases, the new element or component will be added to the page. If there
+   is a component/element with a ``data-live-id`` attribute that is *not* in the
+   new HTML, that component/element will be removed from the page.
 
 Skipping Updating Certain Elements
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -3223,8 +3239,8 @@ an element, that changes is preserved (see :ref:`smart-rerender-algorithm`).
 
 .. note::
 
-    To *force* an ignored element to re-render, give its parent element an
-    ``id`` attribute. During a re-render, if this value changes, all
+    To *force* an ignored element to re-render, give its parent element a
+    ``data-live-id`` attribute. During a re-render, if this value changes, all
     of the children of the element will be re-rendered, even those with ``data-live-ignore``.
 
 Overwrite HTML Instead of Morphing
@@ -3466,4 +3482,3 @@ bound to Symfony's BC policy for the moment.
 .. _`Twig Component debug command`: https://symfony.com/bundles/ux-twig-component/current/index.html#debugging-components
 .. _`PostMount hook`: https://symfony.com/bundles/ux-twig-component/current/index.html#postmount-hook
 .. _`validation groups`: https://symfony.com/doc/current/form/validation_groups.html
-.. _morphing library: https://github.com/bigskysoftware/idiomorph
diff --git a/src/LiveComponent/src/EventListener/InterceptChildComponentRenderSubscriber.php b/src/LiveComponent/src/EventListener/InterceptChildComponentRenderSubscriber.php
index 4f610351..55288365 100644
--- a/src/LiveComponent/src/EventListener/InterceptChildComponentRenderSubscriber.php
+++ b/src/LiveComponent/src/EventListener/InterceptChildComponentRenderSubscriber.php
@@ -53,8 +53,8 @@ class InterceptChildComponentRenderSubscriber implements EventSubscriberInterfac
         $childFingerprints = $parentComponent->getExtraMetadata(self::CHILDREN_FINGERPRINTS_METADATA_KEY);
 
         // get the deterministic id for this child, but without incrementing the counter yet
-        if (isset($event->getInputProps()['id'])) {
-            $deterministicId = $event->getInputProps()['id'];
+        if (isset($event->getInputProps()['data-live-id'])) {
+            $deterministicId = $event->getInputProps()['data-live-id'];
         } else {
             $key = $event->getInputProps()[LiveControllerAttributesCreator::KEY_PROP_NAME] ?? null;
             $deterministicId = $this->getDeterministicIdCalculator()->calculateDeterministicId(increment: false, key: $key);
diff --git a/src/LiveComponent/src/Util/ChildComponentPartialRenderer.php b/src/LiveComponent/src/Util/ChildComponentPartialRenderer.php
index 6bd79511..c1c51b1e 100644
--- a/src/LiveComponent/src/Util/ChildComponentPartialRenderer.php
+++ b/src/LiveComponent/src/Util/ChildComponentPartialRenderer.php
@@ -51,7 +51,7 @@ class ChildComponentPartialRenderer implements ServiceSubscriberInterface
         /*
          * The props passed to create this child HAVE changed.
          * Send back a fake element with:
-         *      * id
+         *      * data-live-id
          *      * data-live-fingerprint-value (new fingerprint)
          *      * data-live-props-value (dehydrated props that "accept updates from parent")
          */
@@ -69,13 +69,12 @@ class ChildComponentPartialRenderer implements ServiceSubscriberInterface
         $readonlyDehydratedProps = $liveMetadata->getOnlyPropsThatAcceptUpdatesFromParent($props);
         $readonlyDehydratedProps = $this->getLiveComponentHydrator()->addChecksumToData($readonlyDehydratedProps);
 
-        $attributesCollection->setPropsUpdatedFromParent($readonlyDehydratedProps);
+        $attributesCollection->setProps($readonlyDehydratedProps);
         $attributes = $attributesCollection->toEscapedArray();
         // optional, but these just aren't needed by the frontend at this point
         unset($attributes['data-controller']);
         unset($attributes['data-live-url-value']);
         unset($attributes['data-live-csrf-value']);
-        unset($attributes['data-live-props-value']);
 
         return $this->createHtml($attributes, $childTag);
     }
@@ -89,7 +88,6 @@ class ChildComponentPartialRenderer implements ServiceSubscriberInterface
         $attributes = array_map(function ($key, $value) {
             return sprintf('%s="%s"', $key, $value);
         }, array_keys($attributes), $attributes);
-        $attributes[] = 'data-live-preserve="true"';
 
         return sprintf('<%s %s></%s>', $childTag, implode(' ', $attributes), $childTag);
     }
diff --git a/src/LiveComponent/src/Util/LiveAttributesCollection.php b/src/LiveComponent/src/Util/LiveAttributesCollection.php
index e73217a8..43046bf6 100644
--- a/src/LiveComponent/src/Util/LiveAttributesCollection.php
+++ b/src/LiveComponent/src/Util/LiveAttributesCollection.php
@@ -49,10 +49,9 @@ final class LiveAttributesCollection
         $this->attributes['data-live-name-value'] = $componentName;
     }
 
-    // TODO rename that
     public function setLiveId(string $id): void
     {
-        $this->attributes['id'] = $id;
+        $this->attributes['data-live-id'] = $id;
     }
 
     public function setFingerprint(string $fingerprint): void
@@ -65,11 +64,6 @@ final class LiveAttributesCollection
         $this->attributes['data-live-props-value'] = $dehydratedProps;
     }
 
-    public function setPropsUpdatedFromParent(array $dehydratedProps): void
-    {
-        $this->attributes['data-live-props-updated-from-parent-value'] = $dehydratedProps;
-    }
-
     public function getProps(): array
     {
         if (!\array_key_exists('data-live-props-value', $this->attributes)) {
@@ -96,12 +90,12 @@ final class LiveAttributesCollection
 
     public function setEventsToEmit(array $events): void
     {
-        $this->attributes['data-live-events-to-emit-value'] = $events;
+        $this->attributes['data-live-emit'] = $events;
     }
 
     public function setBrowserEventsToDispatch(array $browserEventsToDispatch): void
     {
-        $this->attributes['data-live-events-to-dispatch-value'] = $browserEventsToDispatch;
+        $this->attributes['data-live-browser-dispatch'] = $browserEventsToDispatch;
     }
 
     public function setRequestMethod(string $requestMethod): void
diff --git a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
index 25d89e94..81a55a2f 100644
--- a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
+++ b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
@@ -35,7 +35,7 @@ class LiveControllerAttributesCreator
     /**
      * Prop name that can be passed into a component to keep it unique in a loop.
      *
-     * This is used to generate the unique id for the child component.
+     * This is used to generate the unique data-live-id for the child component.
      */
     public const KEY_PROP_NAME = 'key';
 
@@ -89,21 +89,18 @@ class LiveControllerAttributesCreator
             ]);
         }
 
-        if (!isset($mountedAttributes->all()['id'])) {
+        if (!isset($mountedAttributes->all()['data-live-id'])) {
             $id = $deterministicId ?: $this->idCalculator
                 ->calculateDeterministicId(key: $mounted->getInputProps()[self::KEY_PROP_NAME] ?? null);
             $attributesCollection->setLiveId($id);
             // we need to add this to the mounted attributes so that it is
             // will be included in the "attributes" part of the props data.
-            $mountedAttributes = $mountedAttributes->defaults(['id' => $id]);
+            $mountedAttributes = $mountedAttributes->defaults(['data-live-id' => $id]);
         }
 
         $liveMetadata = $this->metadataFactory->getMetadata($mounted->getName());
         $requestMethod = $liveMetadata->getComponentMetadata()?->get('method') ?? 'post';
-        // set attribute if needed
-        if ('post' !== $requestMethod) {
-            $attributesCollection->setRequestMethod($requestMethod);
-        }
+        $attributesCollection->setRequestMethod($requestMethod);
 
         if ($liveMetadata->hasQueryStringBindings()) {
             $queryMapping = [];
diff --git a/src/StimulusBundle/composer.json b/src/StimulusBundle/composer.json
index 5af49cfe..2b0c33ac 100644
--- a/src/StimulusBundle/composer.json
+++ b/src/StimulusBundle/composer.json
@@ -18,7 +18,7 @@
         "symfony/dependency-injection": "^5.4|^6.0|^7.0",
         "symfony/finder": "^5.4|^6.0|^7.0",
         "symfony/http-kernel": "^5.4|^6.0|^7.0",
-        "twig/twig": "^2.15.3|^3.4.3",
+        "twig/twig": "^2.15.3|~3.8.0",
         "symfony/deprecation-contracts": "^2.0|^3.0"
     },
     "require-dev": {
diff --git a/src/StimulusBundle/src/Helper/StimulusHelper.php b/src/StimulusBundle/src/Helper/StimulusHelper.php
index a292e2f0..a433caf1 100644
--- a/src/StimulusBundle/src/Helper/StimulusHelper.php
+++ b/src/StimulusBundle/src/Helper/StimulusHelper.php
@@ -27,9 +27,7 @@ final class StimulusHelper
     public function __construct(?Environment $twig)
     {
         // Twig needed just for its escaping mechanism
-        $this->twig = $twig ?? new Environment(new ArrayLoader(), [
-            'use_yield' => true,
-        ]);
+        $this->twig = $twig ?? new Environment(new ArrayLoader());
     }
 
     public function createStimulusAttributes(): StimulusAttributes
diff --git a/src/Turbo/assets/package.json b/src/Turbo/assets/package.json
index e7da91bf..48404365 100644
--- a/src/Turbo/assets/package.json
+++ b/src/Turbo/assets/package.json
@@ -21,16 +21,16 @@
             }
         },
         "importmap": {
-            "@hotwired/turbo": "^7.1.0 || ^8.0",
+            "@hotwired/turbo": "^7.1.0",
             "@hotwired/stimulus": "^3.0.0"
         }
     },
     "peerDependencies": {
-        "@hotwired/turbo": "^7.1.1 || ^8.0",
+        "@hotwired/turbo": "^7.0.1",
         "@hotwired/stimulus": "^3.0.0"
     },
     "devDependencies": {
-        "@hotwired/turbo": "^7.1.0 || ^8.0",
+        "@hotwired/turbo": "^7.1.0",
         "@hotwired/stimulus": "^3.0.0"
     }
 }
diff --git a/src/TwigComponent/composer.json b/src/TwigComponent/composer.json
index 36f7e696..090ffe23 100644
--- a/src/TwigComponent/composer.json
+++ b/src/TwigComponent/composer.json
@@ -31,7 +31,7 @@
         "symfony/deprecation-contracts": "^2.2|^3.0",
         "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
         "symfony/property-access": "^5.4|^6.0|^7.0",
-        "twig/twig": "^2.14.7|^3.0.4"
+        "twig/twig": "^2.14.7|~3.8.0"
     },
     "require-dev": {
         "symfony/console": "^5.4|^6.0|^7.0",
diff --git a/ux.symfony.com/templates/components/LiveMemory/Card.html.twig b/ux.symfony.com/templates/components/LiveMemory/Card.html.twig
index c7239866..08b3c661 100644
--- a/ux.symfony.com/templates/components/LiveMemory/Card.html.twig
+++ b/ux.symfony.com/templates/components/LiveMemory/Card.html.twig
@@ -1,5 +1,5 @@
 <div
-    id="card-{{ key }}"
+    data-live-id="card-{{ key }}"
     class="{{ html_classes('LiveMemory-Card', {
         'LiveMemory-Card--flipped': flipped,
         'LiveMemory-Card--selected': selected,
diff --git a/ux.symfony.com/templates/components/LiveMemory/Tableau.html.twig b/ux.symfony.com/templates/components/LiveMemory/Tableau.html.twig
index 758e419d..efda789b 100644
--- a/ux.symfony.com/templates/components/LiveMemory/Tableau.html.twig
+++ b/ux.symfony.com/templates/components/LiveMemory/Tableau.html.twig
@@ -11,7 +11,7 @@
             "--slot-y": y
         } %}
         <div class="LiveMemory-CardSlot"
-             id="slot-{{ key }}"
+             data-live-id="slot-{{ key }}"
              style="{{ style|map((v, k) => '%s: %s;'|format(k, v))|join(' ') }}"
         >
 


Test patch:
diff --git a/src/LiveComponent/assets/test/Component/index.test.ts b/src/LiveComponent/assets/test/Component/index.test.ts
index fb9eee10..27ffafb5 100644
--- a/src/LiveComponent/assets/test/Component/index.test.ts
+++ b/src/LiveComponent/assets/test/Component/index.test.ts
@@ -1,10 +1,10 @@
 import Component, { proxifyComponent } from '../../src/Component';
 import {BackendAction, BackendInterface} from '../../src/Backend/Backend';
+import { StandardElementDriver } from '../../src/Component/ElementDriver';
 import BackendRequest from '../../src/Backend/BackendRequest';
 import { Response } from 'node-fetch';
 import { waitFor } from '@testing-library/dom';
 import BackendResponse from '../../src/Backend/BackendResponse';
-import { noopElementDriver } from '../tools';
 
 interface MockBackend extends BackendInterface {
     actions: BackendAction[],
@@ -30,9 +30,11 @@ const makeTestComponent = (): { component: Component, backend: MockBackend } =>
         'test-component',
         { firstName: '', product: { name: '' } },
         [],
+        () => [],
+        null,
         null,
         backend,
-        new noopElementDriver(),
+        new StandardElementDriver()
     );
 
     return {
diff --git a/src/LiveComponent/assets/test/ComponentRegistry.test.ts b/src/LiveComponent/assets/test/ComponentRegistry.test.ts
index 22429f0b..a8d3ffb2 100644
--- a/src/LiveComponent/assets/test/ComponentRegistry.test.ts
+++ b/src/LiveComponent/assets/test/ComponentRegistry.test.ts
@@ -1,14 +1,9 @@
 import Component from '../src/Component';
-import {
-    registerComponent,
-    resetRegistry,
-    getComponent,
-    findComponents,
-} from '../src/ComponentRegistry';
+import ComponentRegistry from '../src/ComponentRegistry';
 import BackendRequest from '../src/Backend/BackendRequest';
 import { BackendInterface } from '../src/Backend/Backend';
 import { Response } from 'node-fetch';
-import { noopElementDriver } from './tools';
+import { StandardElementDriver } from '../src/Component/ElementDriver';
 
 const createComponent = (element: HTMLElement, name = 'foo-component'): Component => {
     const backend: BackendInterface = {
@@ -27,55 +22,61 @@ const createComponent = (element: HTMLElement, name = 'foo-component'): Componen
         name,
         {},
         [],
+        () => [],
+        null,
         null,
         backend,
-        new noopElementDriver(),
+        new StandardElementDriver(),
     );
 };
 
 describe('ComponentRegistry', () => {
-    beforeEach(() => {
-        resetRegistry();
-    });
-
     it('can add and retrieve components', async () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
         const component1 = createComponent(element1);
         const element2 = document.createElement('div');
         const component2 = createComponent(element2);
 
-        registerComponent(component1);
-        registerComponent(component2);
+        registry.registerComponent(element1, component1);
+        registry.registerComponent(element2, component2);
 
-        const promise1 = getComponent(element1);
-        const promise2 = getComponent(element2);
+        const promise1 = registry.getComponent(element1);
+        const promise2 = registry.getComponent(element2);
         await expect(promise1).resolves.toBe(component1);
         await expect(promise2).resolves.toBe(component2);
     });
 
     it('fails if component is not found soon', async () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
-        const promise = getComponent(element1);
+        const promise = registry.getComponent(element1);
         expect.assertions(1);
         await expect(promise).rejects.toEqual(new Error('Component not found for element <div></div>'));
     });
 
     it('can find components in the simple case', () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
         const component1 = createComponent(element1);
         const element2 = document.createElement('div');
         const component2 = createComponent(element2);
 
-        registerComponent(component1);
-        registerComponent(component2);
+        registry.registerComponent(element1, component1);
+        registry.registerComponent(element2, component2);
 
         const otherComponent = createComponent(document.createElement('div'));
 
-        const components = findComponents(otherComponent, false, null);
+        const components = registry.findComponents(otherComponent, false, null);
         expect(components).toEqual([component1, component2]);
     });
 
     it('can find components with only parents', () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
         const component1 = createComponent(element1);
         const element2 = document.createElement('div');
@@ -86,15 +87,17 @@ describe('ComponentRegistry', () => {
         // put component 2 inside component 1
         element1.appendChild(element2);
 
-        registerComponent(component1);
-        registerComponent(component2);
-        registerComponent(component3);
+        registry.registerComponent(element1, component1);
+        registry.registerComponent(element2, component2);
+        registry.registerComponent(element3, component3);
 
-        const components = findComponents(component2, true, null);
+        const components = registry.findComponents(component2, true, null);
         expect(components).toEqual([component1]);
     });
 
     it('can find components by name', () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
         const component1 = createComponent(element1, 'component-type1');
         const element2 = document.createElement('div');
@@ -102,26 +105,28 @@ describe('ComponentRegistry', () => {
         const element3 = document.createElement('div');
         const component3 = createComponent(element3, 'component-type2');
 
-        registerComponent(component1);
-        registerComponent(component2);
-        registerComponent(component3);
+        registry.registerComponent(element1, component1);
+        registry.registerComponent(element2, component2);
+        registry.registerComponent(element3, component3);
 
         const otherComponent = createComponent(document.createElement('div'));
 
-        const components = findComponents(otherComponent, false, 'component-type1');
+        const components = registry.findComponents(otherComponent, false, 'component-type1');
         expect(components).toEqual([component1, component2]);
     });
 
     it('will find components including itself', () => {
+        const registry = new ComponentRegistry();
+
         const element1 = document.createElement('div');
         const component1 = createComponent(element1, 'component-type1');
         const element2 = document.createElement('div');
         const component2 = createComponent(element2, 'component-type1');
 
-        registerComponent(component1);
-        registerComponent(component2);
+        registry.registerComponent(element1, component1);
+        registry.registerComponent(element2, component2);
 
-        const components = findComponents(component2, false, null);
+        const components = registry.findComponents(component2, false, null);
         expect(components).toEqual([component1, component2]);
     });
 });
diff --git a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts b/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts
deleted file mode 100644
index 8d1f457f..00000000
--- a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { htmlToElement } from '../../src/dom_utils';
-import getElementAsTagText from '../../src/Util/getElementAsTagText';
-
-describe('getElementAsTagText', () => {
-    it('returns self-closing tag correctly', () => {
-        const element = htmlToElement('<input name="user[firstName]">');
-
-        expect(getElementAsTagText(element)).toEqual('<input name="user[firstName]">')
-    });
-
-    it('returns tag text without the innerHTML', () => {
-        const element = htmlToElement('<div class="foo">Name: <input name="user[firstName]"></div>');
-
-        expect(getElementAsTagText(element)).toEqual('<div class="foo">')
-    });
-});
diff --git a/src/LiveComponent/assets/test/controller/basic.test.ts b/src/LiveComponent/assets/test/controller/basic.test.ts
index a3e2c9cb..be5f98da 100644
--- a/src/LiveComponent/assets/test/controller/basic.test.ts
+++ b/src/LiveComponent/assets/test/controller/basic.test.ts
@@ -12,8 +12,7 @@
 import {createTest, initComponent, shutdownTests, startStimulus} from '../tools';
 import { htmlToElement } from '../../src/dom_utils';
 import Component from '../../src/Component';
-import { getComponent } from '../../src/live_controller';
-import { findComponents } from '../../src/ComponentRegistry';
+import LiveControllerDefault, { getComponent } from '../../src/live_controller';
 
 describe('LiveController Basic Tests', () => {
     afterEach(() => {
@@ -42,12 +41,13 @@ describe('LiveController Basic Tests', () => {
         expect(test.component).toBeInstanceOf(Component);
         expect(test.component.defaultDebounce).toEqual(115);
         expect(test.component.id).toEqual('the-id');
+        expect(test.component.fingerprint).toEqual('the-fingerprint');
         await expect(getComponent(test.element)).resolves.toBe(test.component);
-        expect(findComponents(test.component, false, null)[0]).toBe(test.component);
+        expect(LiveControllerDefault.componentRegistry.findComponents(test.component, false, null)[0]).toBe(test.component);
 
         // check that it disconnects
         document.body.innerHTML = '';
         await expect(getComponent(test.element)).rejects.toThrow('Component not found for element');
-        expect(findComponents(test.component, false, null)).toEqual([]);
+        expect(LiveControllerDefault.componentRegistry.findComponents(test.component, false, null)).toEqual([]);
     });
 });
diff --git a/src/LiveComponent/assets/test/controller/child-model.test.ts b/src/LiveComponent/assets/test/controller/child-model.test.ts
index 4dd1052d..ad2daad0 100644
--- a/src/LiveComponent/assets/test/controller/child-model.test.ts
+++ b/src/LiveComponent/assets/test/controller/child-model.test.ts
@@ -43,7 +43,7 @@ describe('Component parent -> child data-model binding tests', () => {
             .willReturn((data: any) => `
                 <div ${initComponent(data)}>
                     Food Name ${data.foodName}
-                    <div id="the-child-id">
+                    <div data-live-id="the-child-id">
                 </div>
             `);
 
@@ -76,7 +76,7 @@ describe('Component parent -> child data-model binding tests', () => {
             .willReturn((data: any) => `
                 <div ${initComponent(data)}>
                     Food Name ${data.foodName}
-                    <div id="the-child-id">
+                    <div data-live-id="the-child-id">
                 </div>
             `);
 
@@ -135,7 +135,7 @@ describe('Component parent -> child data-model binding tests', () => {
             .willReturn((data: any) => `
                 <div ${initComponent(data)}>
                     Food Name ${data.foodName}
-                    <div id="the-child-id" data-live-preserve></div>
+                    <div data-live-id="the-child-id">
                 </div>
             `);
 
diff --git a/src/LiveComponent/assets/test/controller/child.test.ts b/src/LiveComponent/assets/test/controller/child.test.ts
index aa9888a9..d0da1fff 100644
--- a/src/LiveComponent/assets/test/controller/child.test.ts
+++ b/src/LiveComponent/assets/test/controller/child.test.ts
@@ -9,23 +9,63 @@
 
 'use strict';
 
-import {
-    createTestForExistingComponent,
-    createTest,
-    initComponent,
-    shutdownTests,
-    getComponent,
-    dataToJsonAttribute,
-} from '../tools';
+import { createTestForExistingComponent, createTest, initComponent, shutdownTests, getComponent } from '../tools';
 import { getByTestId, waitFor } from '@testing-library/dom';
 import userEvent from '@testing-library/user-event';
-import { findChildren } from '../../src/ComponentRegistry';
 
 describe('Component parent -> child initialization and rendering tests', () => {
     afterEach(() => {
         shutdownTests();
     })
 
+    it('adds & removes the child correctly', async () => {
+        const childTemplate = (data: any) => `
+            <div ${initComponent(data, {id: 'the-child-id'})} data-testid="child"></div>
+        `;
+
+        const test = await createTest({}, (data: any) => `
+            <div ${initComponent(data)}>
+                ${childTemplate({})}
+            </div>
+        `);
+
+        const parentComponent = test.component;
+        const childComponent = getComponent(getByTestId(test.element, 'child'));
+        // setting a marker to help verify THIS exact Component instance continues to be used
+        childComponent.fingerprint = 'FOO-FINGERPRINT';
+
+        // check that the relationships all loaded correctly
+        expect(parentComponent.getChildren().size).toEqual(1);
+        // check fingerprint instead of checking object equality with childComponent
+        // because childComponent is actually the proxied Component
+        expect(parentComponent.getChildren().get('the-child-id')?.fingerprint).toEqual('FOO-FINGERPRINT');
+        expect(childComponent.getParent()).toBe(parentComponent);
+
+        // remove the child
+        childComponent.element.remove();
+        // wait because the event is slightly async
+        await waitFor(() => expect(parentComponent.getChildren().size).toEqual(0));
+        expect(childComponent.getParent()).toBeNull();
+
+        // now put it back!
+        test.element.appendChild(childComponent.element);
+        await waitFor(() => expect(parentComponent.getChildren().size).toEqual(1));
+        expect(parentComponent.getChildren().get('the-child-id')?.fingerprint).toEqual('FOO-FINGERPRINT');
+        expect(childComponent.getParent()).toEqual(parentComponent);
+
+        // now remove the whole darn thing!
+        test.element.remove();
+        // this will, while disconnected, break the parent-child bond
+        await waitFor(() => expect(parentComponent.getChildren().size).toEqual(0));
+        expect(childComponent.getParent()).toBeNull();
+
+        // put it *all* back
+        document.body.appendChild(test.element);
+        await waitFor(() => expect(parentComponent.getChildren().size).toEqual(1));
+        expect(parentComponent.getChildren().get('the-child-id')?.fingerprint).toEqual('FOO-FINGERPRINT');
+        expect(childComponent.getParent()).toEqual(parentComponent);
+    });
+
     it('sends a map of child fingerprints on re-render', async () => {
         const test = await createTest({}, (data: any) => `
             <div ${initComponent(data)}>
@@ -60,13 +100,13 @@ describe('Component parent -> child initialization and rendering tests', () => {
             });
 
         expect(test.element).toHaveTextContent('Child Component')
-        expect(findChildren(test.component).length).toEqual(1);
+        expect(test.component.getChildren().size).toEqual(1);
         test.component.render();
         // wait for child to disappear
         await waitFor(() => expect(test.element).toHaveAttribute('busy'));
         await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
         expect(test.element).not.toHaveTextContent('Child Component')
-        expect(findChildren(test.component).length).toEqual(0);
+        expect(test.component.getChildren().size).toEqual(0);
     });
 
     it('adds new child component on re-render', async () => {
@@ -85,23 +125,23 @@ describe('Component parent -> child initialization and rendering tests', () => {
             });
 
         expect(test.element).not.toHaveTextContent('Child Component')
-        expect(findChildren(test.component).length).toEqual(0);
+        expect(test.component.getChildren().size).toEqual(0);
         test.component.render();
         // wait for child to disappear
         await waitFor(() => expect(test.element).toHaveAttribute('busy'));
         await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
         expect(test.element).toHaveTextContent('Child Component')
-        expect(findChildren(test.component).length).toEqual(1);
+        expect(test.component.getChildren().size).toEqual(1);
     });
 
-    it('new child marked as data-live-preserve is ignored except for new attributes', async () => {
+    it('existing child component that has no props is ignored', async () => {
         const originalChild = `
             <div ${initComponent({}, {id: 'the-child-id'})}>
                 Original Child Component
             </div>
         `;
         const updatedChild = `
-            <div id="the-child-id" data-live-preserve data-new="bar">
+            <div ${initComponent({}, {id: 'the-child-id'})}>
                 Updated Child Component
             </div>
         `;
@@ -124,8 +164,6 @@ describe('Component parent -> child initialization and rendering tests', () => {
         await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
         // child component is STILL here: the new rendering was ignored
         expect(test.element).toHaveTextContent('Original Child Component')
-        expect(test.element).toContainHTML('data-new="bar"');
-        expect(test.element).not.toContainHTML('data-live-preserve');
     });
 
     it('existing child component gets props & triggers re-render', async () => {
@@ -141,12 +179,10 @@ describe('Component parent -> child initialization and rendering tests', () => {
 
         // a simpler version of the child is returned from the parent component's re-render
         const childReturnedFromParentCall = `
-            <div
-                id="the-child-id"
-                data-live-fingerprint-value="updated fingerprint"
-                data-live-preserve
-                data-live-props-updated-from-parent-value="${dataToJsonAttribute({toUppercase: true})}"
-            ><!-- no body needed --></div>
+            <div ${initComponent(
+                { toUppercase: true }, // new prop value (firstName is not sent as it is writable)
+                { id: 'the-child-id', fingerprint: 'updated fingerprint'}
+            )}><!-- no body needed --></div>
         `;
 
         const test = await createTest({useOriginalChild: true}, (data: any) => `
@@ -221,17 +257,14 @@ describe('Component parent -> child initialization and rendering tests', () => {
         expect(childComponent.element).toHaveTextContent('Full Name: RYAN WEAVER')
     });
 
-    it('child controller changes its component if child id changes', async () => {
-        // both are a span in the same position: so the same Stimulus controller
-        // will be used for both.
-        const originalChildTemplate = (data: any) => `
-            <span ${initComponent(data, { id: 'original-child-id' })} data-testid="child-component">
+    it('replaces old child with new child if the "id" changes', async () => {
+        const originalChildTemplate = `
+            <span ${initComponent({}, { id: 'original-child-id' })} data-testid="child-component">
                 Original Child
             </span>
         `;
-
-        const reRenderedChildTemplate = (data: any) => `
-            <span ${initComponent(data, { id: 'new-child-id' })} data-testid="child-component">
+        const reRenderedChildTemplate = `
+            <span ${initComponent({}, { id: 'new-child-id' })} data-testid="child-component">
                 New Child
             </span>
         `;
@@ -239,12 +272,10 @@ describe('Component parent -> child initialization and rendering tests', () => {
         const test = await createTest({useOriginalChild: true}, (data: any) => `
            <div ${initComponent(data)}>
                Parent Component
-               ${data.useOriginalChild ? originalChildTemplate({name: 'original'}) : reRenderedChildTemplate({name: 'new'})}
+               ${data.useOriginalChild ? originalChildTemplate : reRenderedChildTemplate}
            </div>
        `);
 
-        const originalChildElement = getByTestId(test.element, 'child-component');
-
         // Re-render the parent
         test.expectsAjaxCall()
             .serverWillChangeProps((data: any) => {
@@ -260,11 +291,7 @@ describe('Component parent -> child initialization and rendering tests', () => {
         expect(test.element).toHaveTextContent('New Child')
         expect(test.element).not.toHaveTextContent('Original Child')
 
-        expect(findChildren(test.component).length).toEqual(1);
-        const newChildElement = getByTestId(test.element, 'child-component');
-        expect(newChildElement).toEqual(originalChildElement);
-        const childComponent = getComponent(newChildElement);
-        expect(childComponent.id).toEqual('new-child-id');
+        expect(test.component.getChildren().size).toEqual(1);
     });
 
     it('tracks various children correctly, even if position changes', async () => {
@@ -274,13 +301,8 @@ describe('Component parent -> child initialization and rendering tests', () => {
             </span>
         `;
         // the empty-ish child element used on re-render
-        const emptyChildTemplate = (data: any) => `
-            <span
-                id="child-id-${data.number}"
-                data-testid="child-component-${data.number}"
-                data-live-preserve
-                data-live-props-updated-from-parent-value="${dataToJsonAttribute({ number: data.number, value: data.value })}"
-            ></span>
+        const childRenderedFromParentTemplate = (data: any) => `
+            <span ${initComponent({ number: data.number, value: data.value }, { id: `child-id-${data.number}` })} data-testid="child-component-${data.number}"></span>
         `;
 
         const test = await createTest({}, (data: any) => `
@@ -298,16 +320,19 @@ describe('Component parent -> child initialization and rendering tests', () => {
             .willReturn((data: any) => `
                 <div ${initComponent(data)}>
                     <div id="foo">
-                        ${emptyChildTemplate({ number: 2, value: 'New value for child 2' })}
+                        ${childRenderedFromParentTemplate({ number: 2, value: 'New value for child 2' })}
                     </div>
                     <div>Parent Component Updated</div>
                     <ul>
                         <li>
-                            ${emptyChildTemplate({ number: 1, value: 'New value for child 1' })}
+                            ${childRenderedFromParentTemplate({ number: 1, value: 'New value for child 1' })}
                         </li>
                     </ul>
                 </div>
             `);
+        test.component.render();
+        // wait for parent Ajax call to start
+        await waitFor(() => expect(test.element).toHaveAttribute('busy'));
 
         const childComponent1 = getComponent(getByTestId(test.element, 'child-component-1'));
         const childTest1 = createTestForExistingComponent(childComponent1);
@@ -326,15 +351,12 @@ describe('Component parent -> child initialization and rendering tests', () => {
             .expectUpdatedPropsFromParent({ number: 2, value: 'New value for child 2' })
             .willReturn(childTemplate);
 
-        // trigger the parent render, which will trigger the children to re-render
-        test.component.render();
-
         // wait for parent Ajax call to finish
         await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
-
         // wait for child to start and stop loading
-        await waitFor(() => expect(getByTestId(test.element, 'child-component-1')).not.toHaveAttribute('busy'));
-        await waitFor(() => expect(getByTestId(test.element, 'child-component-2')).not.toHaveAttribute('busy'));
+        await waitFor(() => expect(childTest1.element).toHaveAttribute('busy'));
+        await waitFor(() => expect(childTest1.element).not.toHaveAttribute('busy'));
+        await waitFor(() => expect(childTest2.element).not.toHaveAttribute('busy'));
 
         expect(test.element).toHaveTextContent('Child number: 1 value "New value for child 1"');
         expect(test.element).toHaveTextContent('Child number: 2 value "New value for child 2"');
@@ -342,6 +364,6 @@ describe('Component parent -> child initialization and rendering tests', () => {
         expect(test.element).not.toHaveTextContent('Child number: 2 value "Original value for child 2"');
         // make sure child 2 is in the correct spot
         expect(test.element.querySelector('#foo')).toHaveTextContent('Child number: 2 value "New value for child 2"');
-        expect(findChildren(test.component).length).toEqual(2);
+        expect(test.component.getChildren().size).toEqual(2);
     });
 });
diff --git a/src/LiveComponent/assets/test/controller/emit.test.ts b/src/LiveComponent/assets/test/controller/emit.test.ts
index 15f739ad..50929f2b 100644
--- a/src/LiveComponent/assets/test/controller/emit.test.ts
+++ b/src/LiveComponent/assets/test/controller/emit.test.ts
@@ -64,39 +64,4 @@ describe('LiveController Emit Tests', () => {
         // wait a tiny bit - enough for a request to be sent if it was going to be
         await new Promise((resolve) => setTimeout(resolve, 10));
     });
-
-    it('emits event sent back after Ajax call', async () => {
-        const test = await createTest({ renderCount: 0 }, (data: any) => `
-            <div ${initComponent(data, {
-                name: 'simple-component',
-                listeners: [{ event: 'fooEvent', action: 'fooAction' }],
-                // emit only on the first re-render
-                eventEmit: data.renderCount === 1 ? [
-                    { event: 'fooEvent', data: { foo: 'bar' } }
-                ] : [],
-            })}>
-                Render Count: ${data.renderCount}
-                <button
-                    data-action="live#emit"
-                    data-event="fooEvent"
-                >Emit Simple</button>
-            </div>
-        `);
-
-        test.expectsAjaxCall()
-            .serverWillChangeProps((data) => {
-                data.renderCount = 1;
-            })
-
-        test.component.render();
-
-        test.expectsAjaxCall()
-            .expectActionCalled('fooAction', { foo: 'bar' })
-            .serverWillChangeProps((data) => {
-                data.renderCount = 2;
-            });
-
-        await waitFor(() => expect(test.element).toHaveTextContent('Render Count: 1'));
-        await waitFor(() => expect(test.element).toHaveTextContent('Render Count: 2'));
-    });
 });
diff --git a/src/LiveComponent/assets/test/controller/loading.test.ts b/src/LiveComponent/assets/test/controller/loading.test.ts
index ee33967e..34b27c6d 100644
--- a/src/LiveComponent/assets/test/controller/loading.test.ts
+++ b/src/LiveComponent/assets/test/controller/loading.test.ts
@@ -23,6 +23,7 @@ describe('LiveController data-loading Tests', () => {
             <div ${initComponent(data)}>
                 <span>I like: ${data.food}</span> 
                 <span data-loading="show" data-testid="loading-element">Loading...</span>
+                <button data-action="live#$render">Re-Render</button>
             </div>
         `);
 
@@ -37,7 +38,7 @@ describe('LiveController data-loading Tests', () => {
         // wait for element to hide itself on start up
         await waitFor(() => expect(getByTestId(test.element, 'loading-element')).not.toBeVisible());
 
-        test.component.render();
+        getByText(test.element, 'Re-Render').click();
         // element should instantly be visible
         expect(getByTestId(test.element, 'loading-element')).toBeVisible();
 
@@ -225,31 +226,23 @@ describe('LiveController data-loading Tests', () => {
 
     it('does not trigger loading inside component children', async () => {
        const childTemplate = (data: any) => `
-            <div
-                ${initComponent(data)}
-                id="child-id"
-                data-testid="child"
-                ${data.renderChild ? '' : 'data-live-preserve'}
-            >
+            <div ${initComponent(data, {id: 'child-id'})} data-testid="child">
                 <span data-loading="show" data-testid="child-loading-element-showing">Loading...</span>
                 <span data-loading="hide" data-testid="child-loading-element-hiding">Loading...</span>
             </div>
         `;
 
-        const test = await createTest({renderChild: true} , (data: any) => `
+        const test = await createTest({} , (data: any) => `
             <div ${initComponent(data, {id: 'parent-id'})} data-testid="parent">
                 <span data-loading="show" data-testid="parent-loading-element-showing">Loading...</span>
                 <span data-loading="hide" data-testid="parent-loading-element-hiding">Loading...</span>
-                ${childTemplate({renderChild: data.renderChild})}
+                ${childTemplate({})}
                 <button data-action="live#$render">Render</button>
             </div>
         `);
 
         test.expectsAjaxCall()
             // delay so we can check loading
-            .serverWillChangeProps((data: any) => {
-                data.renderChild = false;
-            })
             .delayResponse(20);
 
         // All showing elements should be hidden / hiding elements should be visible
diff --git a/src/LiveComponent/assets/test/controller/model.test.ts b/src/LiveComponent/assets/test/controller/model.test.ts
index 81ef9189..ca31e38e 100644
--- a/src/LiveComponent/assets/test/controller/model.test.ts
+++ b/src/LiveComponent/assets/test/controller/model.test.ts
@@ -705,15 +705,11 @@ describe('LiveController data-model Tests', () => {
     });
 
     it('does not try to set the value of inputs inside a child component', async () => {
-        const test = await createTest({ comment: 'cookie', childComment: 'mmmm', skipChild: false }, (data: any) => `
+        const test = await createTest({ comment: 'cookie', childComment: 'mmmm' }, (data: any) => `
             <div ${initComponent(data)}>
                 <textarea data-model="comment" id="parent-comment"></textarea>
 
-                <div
-                    ${initComponent({ comment: data.childComment })}
-                    id="the-child-id"
-                    ${data.skipChild ? 'data-live-preserve' : ''}
-                >
+                <div ${initComponent({ comment: data.childComment }, {id: 'the-child-id'})}>
                     <textarea data-model="comment" id="child-comment"></textarea>
                 </div>
             </div>
@@ -736,7 +732,6 @@ describe('LiveController data-model Tests', () => {
             // change the data to be extra tricky
             .serverWillChangeProps((data) => {
                 data.comment = 'i like apples';
-                data.skipChild = true;
             });
 
         await test.component.render();
diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts
index df096692..ced36a3a 100644
--- a/src/LiveComponent/assets/test/controller/render.test.ts
+++ b/src/LiveComponent/assets/test/controller/render.test.ts
@@ -179,10 +179,10 @@ describe('LiveController rendering Tests', () => {
         expect(test.element.innerHTML).toContain('I should not be removed');
     });
 
-    it('if id changes, data-live-ignore elements ARE re-rendered', async () => {
+    it('if data-live-id changes, data-live-ignore elements ARE re-rendered', async () => {
         const test = await createTest({ firstName: 'Ryan', containerId: 'original' }, (data: any) => `
             <div ${initComponent(data)}>
-                <div id="${data.containerId}">
+                <div data-live-id="${data.containerId}">
                     <div data-live-ignore>Inside Ignore Name: <span>${data.firstName}</span></div>
                 </div>
                 
diff --git a/src/LiveComponent/assets/test/dom_utils.test.ts b/src/LiveComponent/assets/test/dom_utils.test.ts
index a90b37b9..42ad09a2 100644
--- a/src/LiveComponent/assets/test/dom_utils.test.ts
+++ b/src/LiveComponent/assets/test/dom_utils.test.ts
@@ -4,13 +4,13 @@ import {
     htmlToElement,
     getModelDirectiveFromElement,
     elementBelongsToThisComponent,
+    getElementAsTagText,
     setValueOnElement
 } from '../src/dom_utils';
 import ValueStore from '../src/Component/ValueStore';
 import Component from '../src/Component';
 import Backend from '../src/Backend/Backend';
-import {StimulusElementDriver} from '../src/Component/ElementDriver';
-import { noopElementDriver } from './tools';
+import {StandardElementDriver} from '../src/Component/ElementDriver';
 
 const createStore = function(props: any = {}): ValueStore {
     return new ValueStore(props);
@@ -266,17 +266,21 @@ describe('getModelDirectiveFromInput', () => {
 });
 
 describe('elementBelongsToThisComponent', () => {
-    const createComponent = (html: string) => {
+    const createComponent = (html: string, childComponents: Component[] = []) => {
         const component = new Component(
             htmlToElement(html),
             'some-component',
             {},
             [],
+            () => [],
             null,
+            'some-id-' + Math.floor((Math.random() * 100)),
             new Backend(''),
-            new noopElementDriver(),
+            new StandardElementDriver()
         );
-        component.connect();
+        childComponents.forEach((childComponent) => {
+            component.addChild(childComponent);
+        })
 
         return component;
     };
@@ -301,23 +305,37 @@ describe('elementBelongsToThisComponent', () => {
         const childComponent = createComponent('<div class="child"></div>');
         childComponent.element.appendChild(targetElement);
 
-        const component = createComponent('<div class="parent"></div>');
+        const component = createComponent('<div class="parent"></div>', [childComponent]);
         component.element.appendChild(childComponent.element);
 
-        //expect(elementBelongsToThisComponent(targetElement, childComponent)).toBeTruthy();
+        expect(elementBelongsToThisComponent(targetElement, childComponent)).toBeTruthy();
         expect(elementBelongsToThisComponent(targetElement, component)).toBeFalsy();
     });
 
     it('returns false if element *is* a child controller element', () => {
         const childComponent = createComponent('<div class="child"></div>');
 
-        const component = createComponent('<div class="parent"></div>');
+        const component = createComponent('<div class="parent"></div>', [childComponent]);
         component.element.appendChild(childComponent.element);
 
         expect(elementBelongsToThisComponent(childComponent.element, component)).toBeFalsy();
     });
 });
 
+describe('getElementAsTagText', () => {
+    it('returns self-closing tag correctly', () => {
+        const element = htmlToElement('<input name="user[firstName]">');
+
+        expect(getElementAsTagText(element)).toEqual('<input name="user[firstName]">')
+    });
+
+    it('returns tag text without the innerHTML', () => {
+        const element = htmlToElement('<div class="foo">Name: <input name="user[firstName]"></div>');
+
+        expect(getElementAsTagText(element)).toEqual('<div class="foo">')
+    });
+});
+
 describe('htmlToElement', () => {
     it('allows to clone HTMLElement', () => {
         const element = htmlToElement('<div class="foo">bar</div>');
diff --git a/src/LiveComponent/assets/test/tools.ts b/src/LiveComponent/assets/test/tools.ts
index 4c70596d..29d1ac97 100644
--- a/src/LiveComponent/assets/test/tools.ts
+++ b/src/LiveComponent/assets/test/tools.ts
@@ -7,8 +7,6 @@ import { BackendAction, BackendInterface, ChildrenFingerprints } from '../src/Ba
 import BackendRequest from '../src/Backend/BackendRequest';
 import { Response } from 'node-fetch';
 import { setDeepData } from '../src/data_manipulation_utils';
-import LiveControllerDefault from '../src/live_controller';
-import { ElementDriver } from '../src/Component/ElementDriver';
 
 let activeTests: FunctionalTest[] = [];
 
@@ -44,11 +42,13 @@ class FunctionalTest {
     template: (props: any) => string;
     mockedBackend: MockedBackend;
 
-    constructor(component: Component, element: HTMLElement, mockedBackend: MockedBackend, template: (props: any) => string) {
+    constructor(component: Component, element: HTMLElement, template: (props: any) => string) {
         this.component = component;
         this.element = element;
-        this.mockedBackend = mockedBackend;
         this.template = template;
+
+        this.mockedBackend = new MockedBackend();
+        this.component._swapBackend(this.mockedBackend);
     }
 
     expectsAjaxCall = (): MockedAjaxCall => {
@@ -350,14 +350,10 @@ class MockedAjaxCall {
     }
 }
 
-const mockBackend = new MockedBackend();
-
 export async function createTest(props: any, template: (props: any) => string): Promise<FunctionalTest> {
-    LiveControllerDefault.backendFactory = () => mockBackend;
-
     const testData = await startStimulus(template(props));
 
-    const test = new FunctionalTest(testData.controller.component, testData.element, mockBackend, template);
+    const test = new FunctionalTest(testData.controller.component, testData.element, template);
     activeTests.push(test);
 
     return test;
@@ -366,7 +362,7 @@ export async function createTest(props: any, template: (props: any) => string):
  * An internal way to create a FunctionalTest: useful for child components
  */
 export function createTestForExistingComponent(component: Component): FunctionalTest {
-    const test = new FunctionalTest(component, component.element, mockBackend, () => '');
+    const test = new FunctionalTest(component, component.element, () => '');
     activeTests.push(test);
 
     return test;
@@ -412,7 +408,7 @@ const getControllerElement = (container: HTMLElement): HTMLElement => {
     return element;
 };
 
-export const dataToJsonAttribute = (data: any): string => {
+const dataToJsonAttribute = (data: any): string => {
     const container = document.createElement('div');
     container.dataset.foo = JSON.stringify(data);
 
@@ -434,11 +430,10 @@ export function initComponent(props: any = {}, controllerValues: any = {}) {
         data-live-props-value="${dataToJsonAttribute(props)}"
         ${controllerValues.debounce ? `data-live-debounce-value="${controllerValues.debounce}"` : ''}
         ${controllerValues.csrf ? `data-live-csrf-value="${controllerValues.csrf}"` : ''}
-        ${controllerValues.id ? `id="${controllerValues.id}"` : ''}
+        ${controllerValues.id ? `data-live-id="${controllerValues.id}"` : ''}
         ${controllerValues.fingerprint ? `data-live-fingerprint-value="${controllerValues.fingerprint}"` : ''}
         ${controllerValues.listeners ? `data-live-listeners-value="${dataToJsonAttribute(controllerValues.listeners)}"` : ''}
-        ${controllerValues.eventEmit ? `data-live-events-to-emit-value="${dataToJsonAttribute(controllerValues.eventEmit)}"` : ''}
-        ${controllerValues.browserDispatch ? `data-live-events-to-dispatch-value="${dataToJsonAttribute(controllerValues.browserDispatch)}"` : ''}
+        ${controllerValues.browserDispatch ? `data-live-browser-dispatch="${dataToJsonAttribute(controllerValues.browserDispatch)}"` : ''}
         ${controllerValues.queryMapping ? `data-live-query-mapping-value="${dataToJsonAttribute(controllerValues.queryMapping)}"` : ''}
     `;
 }
@@ -464,26 +459,3 @@ export function setCurrentSearch(search: string){
 export function expectCurrentSearch (){
     return expect(decodeURIComponent(window.location.search));
 }
-
-export class noopElementDriver implements ElementDriver {
-    getBrowserEventsToDispatch(): Array<{ event: string; payload: any }> {
-        throw new Error('Method not implemented.');
-    }
-
-    getComponentProps(): any {
-        throw new Error('Method not implemented.');
-    }
-
-    getEventsToEmit(): Array<{
-        event: string;
-        data: any;
-        target: string | null;
-        componentName: string | null
-    }> {
-        throw new Error('Method not implemented.');
-    }
-
-    getModelName(element: HTMLElement): string | null {
-        throw new Error('Method not implemented.');
-    }
-}
diff --git a/src/LiveComponent/src/Test/TestLiveComponent.php b/src/LiveComponent/src/Test/TestLiveComponent.php
index 0a21f33d..c1e2b264 100644
--- a/src/LiveComponent/src/Test/TestLiveComponent.php
+++ b/src/LiveComponent/src/Test/TestLiveComponent.php
@@ -42,7 +42,7 @@ final class TestLiveComponent
     ) {
         $this->client->catchExceptions(false);
 
-        $data['attributes']['id'] ??= 'in-a-real-scenario-it-would-already-have-one---provide-one-yourself-if-needed';
+        $data['attributes']['data-live-id'] ??= 'in-a-real-scenario-it-would-already-have-one---provide-one-yourself-if-needed';
 
         $mounted = $this->factory->create($this->metadata->getName(), $data);
         $props = $this->hydrator->dehydrate(
diff --git a/src/LiveComponent/tests/Fixtures/templates/components/todo_list.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/todo_list.html.twig
index cd0489ab..90c642e2 100644
--- a/src/LiveComponent/tests/Fixtures/templates/components/todo_list.html.twig
+++ b/src/LiveComponent/tests/Fixtures/templates/components/todo_list.html.twig
@@ -5,7 +5,7 @@
     {% for item in items %}
         {% set componentProps = { text: item.text, textLength: item.text|length } %}
         {% if includeDataLiveId %}
-            {% set componentProps = componentProps|merge({id: ('todo-item-' ~ loop.index) }) %}
+            {% set componentProps = componentProps|merge({'data-live-id': ('todo-item-' ~ loop.index) }) %}
         {% endif %}
         {% if loop.index is odd %}
             {{ component('todo_item', componentProps) }}
diff --git a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php
index 7b40968a..7162f7fc 100644
--- a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php
@@ -50,7 +50,7 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
         $this->assertSame(1, $props['count']);
         $this->assertArrayHasKey('@checksum', $props);
         $this->assertArrayHasKey('@attributes', $props);
-        $this->assertArrayHasKey('id', $props['@attributes']);
+        $this->assertArrayHasKey('data-live-id', $props['@attributes']);
     }
 
     public function testCanUseCustomAttributesVariableName(): void
@@ -99,13 +99,13 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
 
         $lis = $ul->children('li');
         // deterministic id: should not change, and counter should increase
-        $this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'0', $lis->first()->attr('id'));
-        $this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'1', $lis->last()->attr('id'));
+        $this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'0', $lis->first()->attr('data-live-id'));
+        $this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'1', $lis->last()->attr('data-live-id'));
 
-        // the id attribute also needs to be part of the "props" so that it persists on renders
+        // the data-live-id attribute also needs to be part of the "props" so that it persists on renders
         $props = json_decode($lis->first()->attr('data-live-props-value'), true);
         $attributesProps = $props['@attributes'];
-        $this->assertArrayHasKey('id', $attributesProps);
+        $this->assertArrayHasKey('data-live-id', $attributesProps);
 
         // fingerprints
         // first and last both have the same length "text" input, and since "textLength"
@@ -130,9 +130,9 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
         ;
 
         $lis = $ul->children('li');
-        // deterministic id: is not used: id was passed in manually
-        $this->assertSame('todo-item-1', $lis->first()->attr('id'));
-        $this->assertSame('todo-item-3', $lis->last()->attr('id'));
+        // deterministic id: is not used: data-live-id was passed in manually
+        $this->assertSame('todo-item-1', $lis->first()->attr('data-live-id'));
+        $this->assertSame('todo-item-3', $lis->last()->attr('data-live-id'));
     }
 
     public function testQueryStringMappingAttribute()
@@ -174,7 +174,7 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
         $this->assertCount(3, $props);
         $this->assertArrayHasKey('@checksum', $props);
         $this->assertArrayHasKey('@attributes', $props);
-        $this->assertArrayHasKey('id', $props['@attributes']);
+        $this->assertArrayHasKey('data-live-id', $props['@attributes']);
         $this->assertArrayHasKey('count', $props);
         $this->assertSame($props['count'], 0);
     }
@@ -209,7 +209,7 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
         $this->assertCount(3, $props);
         $this->assertArrayHasKey('@checksum', $props);
         $this->assertArrayHasKey('@attributes', $props);
-        $this->assertArrayHasKey('id', $props['@attributes']);
+        $this->assertArrayHasKey('data-live-id', $props['@attributes']);
         $this->assertArrayHasKey('count', $props);
         $this->assertSame($props['count'], 2);
     }
diff --git a/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
index d243ccba..b341e9cd 100644
--- a/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/DeferLiveComponentSubscriberTest.php
@@ -35,7 +35,7 @@ final class DeferLiveComponentSubscriberTest extends KernelTestCase
         $this->assertSame('live:connect->live#$render', $div->attr('data-action'));
 
         $component = $this->mountComponent('deferred_component', [
-            'id' => $div->attr('id'),
+            'data-live-id' => $div->attr('data-live-id'),
         ]);
 
         $dehydrated = $this->dehydrateComponent($component);
@@ -62,7 +62,7 @@ final class DeferLiveComponentSubscriberTest extends KernelTestCase
         $this->assertSame('I\'m loading a reaaaally slow live component', trim($div->html()));
 
         $component = $this->mountComponent('deferred_component', [
-            'id' => $div->attr('id'),
+            'data-live-id' => $div->attr('data-live-id'),
         ]);
 
         $dehydrated = $this->dehydrateComponent($component);
diff --git a/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
index 3dfd77bb..90b8fd96 100644
--- a/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php
@@ -86,19 +86,19 @@ final class InterceptChildComponentRenderSubscriberTest extends KernelTestCase
             ->use(function (AbstractBrowser $browser) {
                 $content = $browser->getResponse()->getContent();
 
-                // 1st renders empty
-                // fingerprint changed in 2nd & 3rd, so it renders new fingerprint + props
+                // 1st and 3rd render empty
+                // fingerprint changed in 2nd, so it renders new fingerprint + props
                 $this->assertStringContainsString(sprintf(
-                    '<li id="%s0" data-live-preserve="true"></li>',
+                    '<li data-live-id="%s0"></li>',
                     AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
                 ), $content);
                 // new props are JUST the "textLength" + a checksum for it specifically
                 $this->assertStringContainsString(sprintf(
-                    '<li data-live-name-value="todo_item" id="%s0" data-live-fingerprint-value="sMvvf7q68tz&#x2F;Cuk&#x2B;vDeisDiq&#x2B;7YPWzT&#x2B;WZFzI37dGHY&#x3D;" data-live-props-updated-from-parent-value="&#x7B;&quot;textLength&quot;&#x3A;18,&quot;&#x40;checksum&quot;&#x3A;&quot;LGxXa9fMKrJ6PelkUPfqmdwnfkk&#x2B;LORgoJHXyPpS3Pw&#x3D;&quot;&#x7D;" data-live-preserve="true"></li>',
+                    '<li data-live-name-value="todo_item" data-live-id="%s0" data-live-request-method-value="post" data-live-fingerprint-value="sMvvf7q68tz&#x2F;Cuk&#x2B;vDeisDiq&#x2B;7YPWzT&#x2B;WZFzI37dGHY&#x3D;" data-live-props-value="&#x7B;&quot;textLength&quot;&#x3A;18,&quot;&#x40;checksum&quot;&#x3A;&quot;LGxXa9fMKrJ6PelkUPfqmdwnfkk&#x2B;LORgoJHXyPpS3Pw&#x3D;&quot;&#x7D;"></li>',
                     AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED
                 ), $content);
                 $this->assertStringContainsString(sprintf(
-                    '<li data-live-name-value="todo_item" id="%s1" data-live-fingerprint-value="8AooEz36WYQyxj54BCaDm&#x2F;jKbcdDdPDLaNO4&#x2F;49bcQk&#x3D;" data-live-props-updated-from-parent-value="&#x7B;&quot;textLength&quot;&#x3A;10,&quot;&#x40;checksum&quot;&#x3A;&quot;BXUk7q6LI&#x5C;&#x2F;6Qx3c62Xiui6287YndmoK3QmVq6e5mcGk&#x3D;&quot;&#x7D;" data-live-preserve="true"></li>',
+                    '<li data-live-name-value="todo_item" data-live-id="%s1" data-live-request-method-value="post" data-live-fingerprint-value="8AooEz36WYQyxj54BCaDm&#x2F;jKbcdDdPDLaNO4&#x2F;49bcQk&#x3D;" data-live-props-value="&#x7B;&quot;textLength&quot;&#x3A;10,&quot;&#x40;checksum&quot;&#x3A;&quot;BXUk7q6LI&#x5C;&#x2F;6Qx3c62Xiui6287YndmoK3QmVq6e5mcGk&#x3D;&quot;&#x7D;"></li>',
                     AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
                 ), $content);
             });
@@ -129,12 +129,12 @@ final class InterceptChildComponentRenderSubscriberTest extends KernelTestCase
             ->assertHtml()
             ->assertElementCount('ul li', 3)
             // check for the live-id we expect based on the key
-            ->assertContains('id="live-521026374-the-key0"')
+            ->assertContains('data-live-id="live-521026374-the-key0"')
             ->assertNotContains('key="the-key0"')
             ->visit($urlWithChangedFingerprints)
-            ->assertContains('<li id="live-521026374-the-key0" data-live-preserve="true"></li>')
+            ->assertContains('<li data-live-id="live-521026374-the-key0"></li>')
             // this one is changed, so it renders a full element
-            ->assertContains('<li data-live-name-value="todo_item" id="live-4172682817-the-key1"')
+            ->assertContains('<li data-live-name-value="todo_item" data-live-id="live-4172682817-the-key1"')
         ;
     }
 
diff --git a/src/LiveComponent/tests/Functional/LiveResponderTest.php b/src/LiveComponent/tests/Functional/LiveResponderTest.php
index f47b60d2..833713bf 100644
--- a/src/LiveComponent/tests/Functional/LiveResponderTest.php
+++ b/src/LiveComponent/tests/Functional/LiveResponderTest.php
@@ -53,7 +53,7 @@ final class LiveResponderTest extends KernelTestCase
         ;
 
         $div = $crawler->filter('div');
-        $browserDispatch = $div->attr('data-live-events-to-dispatch-value');
+        $browserDispatch = $div->attr('data-live-browser-dispatch');
         $this->assertNotNull($browserDispatch);
         $browserDispatchData = json_decode($browserDispatch, true);
         $this->assertSame([
diff --git a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
index 2b5ea237..254e09dc 100644
--- a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
+++ b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
@@ -34,7 +34,7 @@ final class DataModelPropsSubscriberTest extends KernelTestCase
             // Normally createAndRender is always called from within a Template via the ComponentExtension.
             // To avoid that the DeterministicTwigIdCalculator complains that there's no Template
             // to base the live id on, we'll add this dummy one, so it gets skipped.
-            'attributes' => ['id' => 'dummy-live-id'],
+            'attributes' => ['data-live-id' => 'dummy-live-id'],
         ]);
 
         $this->assertStringContainsString('<textarea data-model="content:value">Hello data-model!</textarea>', $html);
@@ -53,7 +53,7 @@ final class DataModelPropsSubscriberTest extends KernelTestCase
         $renderer = self::getContainer()->get('ux.twig_component.component_renderer');
 
         $html = $renderer->createAndRender('parent_component_data_model', [
-            'attributes' => ['id' => 'dummy-live-id'],
+            'attributes' => ['data-live-id' => 'dummy-live-id'],
         ]);
 
         $this->assertStringContainsString('<textarea data-model="content">default content on mount</textarea>', $html);
diff --git a/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php b/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
index 350ce2bf..0c19c6a7 100644
--- a/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
+++ b/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
@@ -31,7 +31,7 @@ final class LiveComponentRuntimeTest extends KernelTestCase
             'prop1' => null,
             'prop2' => new \DateTime('2022-10-06-0'),
             'prop3' => 'howdy',
-            'attributes' => ['id' => 'in-a-real-scenario-it-would-already-have-one'],
+            'attributes' => ['data-live-id' => 'in-a-real-scenario-it-would-already-have-one'],
         ]);
 
         $this->assertStringStartsWith('/_components/component1?props=%7B%22prop1%22:null,%22prop2%22:%222022-10-06T00:00:00%2B00:00%22,%22prop3%22:%22howdy%22,%22', $url);
diff --git a/src/LiveComponent/tests/LiveComponentTestHelper.php b/src/LiveComponent/tests/LiveComponentTestHelper.php
index 831a7f30..142528c5 100644
--- a/src/LiveComponent/tests/LiveComponentTestHelper.php
+++ b/src/LiveComponent/tests/LiveComponentTestHelper.php
@@ -43,8 +43,8 @@ trait LiveComponentTestHelper
 
     private function mountComponent(string $name, array $data = [], $addDummyLiveId = true): MountedComponent
     {
-        if ($addDummyLiveId && empty($data['attributes']['id'])) {
-            $data['attributes']['id'] = 'in-a-real-scenario-it-would-already-have-one';
+        if ($addDummyLiveId && empty($data['attributes']['data-live-id'])) {
+            $data['attributes']['data-live-id'] = 'in-a-real-scenario-it-would-already-have-one';
         }
 
         return $this->factory()->create($name, $data);
diff --git a/src/LiveComponent/tests/Unit/Util/LiveAttributesCollectionTest.php b/src/LiveComponent/tests/Unit/Util/LiveAttributesCollectionTest.php
index ba933cf9..eea84503 100644
--- a/src/LiveComponent/tests/Unit/Util/LiveAttributesCollectionTest.php
+++ b/src/LiveComponent/tests/Unit/Util/LiveAttributesCollectionTest.php
@@ -51,13 +51,13 @@ class LiveAttributesCollectionTest extends KernelTestCase
         $expected = [
             'data-controller' => 'live',
             'data-live-name-value' => 'my-component',
-            'id' => 'the-live-id',
+            'data-live-id' => 'the-live-id',
             'data-live-fingerprint-value' => 'the-fingerprint',
             'data-live-props-value' => '&#x7B;&quot;the&quot;&#x3A;&quot;props&quot;&#x7D;',
             'data-live-url-value' => 'the-live-url',
             'data-live-csrf-value' => 'the-csrf-token',
             'data-live-listeners-value' => '&#x7B;&quot;event_name&quot;&#x3A;&quot;theActionName&quot;&#x7D;',
-            'data-live-events-to-emit-value' => '&#x5B;&#x7B;&quot;event&quot;&#x3A;&quot;event_name1&quot;,&quot;data&quot;&#x3A;&#x7B;&quot;the&quot;&#x3A;&quot;data&quot;&#x7D;,&quot;target&quot;&#x3A;&quot;up&quot;,&quot;componentName&quot;&#x3A;&quot;the-component&quot;&#x7D;,&#x7B;&quot;event&quot;&#x3A;&quot;event_name2&quot;,&quot;data&quot;&#x3A;&#x7B;&quot;the&quot;&#x3A;&quot;data&quot;&#x7D;,&quot;target&quot;&#x3A;null,&quot;componentName&quot;&#x3A;null&#x7D;&#x5D;',
+            'data-live-emit' => '&#x5B;&#x7B;&quot;event&quot;&#x3A;&quot;event_name1&quot;,&quot;data&quot;&#x3A;&#x7B;&quot;the&quot;&#x3A;&quot;data&quot;&#x7D;,&quot;target&quot;&#x3A;&quot;up&quot;,&quot;componentName&quot;&#x3A;&quot;the-component&quot;&#x7D;,&#x7B;&quot;event&quot;&#x3A;&quot;event_name2&quot;,&quot;data&quot;&#x3A;&#x7B;&quot;the&quot;&#x3A;&quot;data&quot;&#x7D;,&quot;target&quot;&#x3A;null,&quot;componentName&quot;&#x3A;null&#x7D;&#x5D;',
             'data-live-query-mapping-value' => '&#x7B;&quot;foo&quot;&#x3A;&#x7B;&quot;name&quot;&#x3A;&quot;foo&quot;&#x7D;,&quot;bar&quot;&#x3A;&#x7B;&quot;name&quot;&#x3A;&quot;bar&quot;&#x7D;&#x7D;',
         ];
 
diff --git a/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php b/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
index 1039ed17..7d76ebe2 100644
--- a/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
+++ b/src/StimulusBundle/tests/Dto/StimulusAttributesTest.php
@@ -24,9 +24,7 @@ final class StimulusAttributesTest extends TestCase
 
     protected function setUp(): void
     {
-        $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader(), [
-            'use_yield' => true,
-        ]));
+        $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader()));
     }
 
     public function testAddAction(): void
diff --git a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
index f53c0351..7cad387a 100644
--- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
+++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
@@ -133,9 +133,7 @@ final class ComponentAttributesTest extends TestCase
             'data-live-data-value' => '{}',
         ]);
 
-        $stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader(), [
-            'use_yield' => true,
-        ]));
+        $stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader()));
         $stimulusAttributes->addController('foo', ['name' => 'ryan', 'some_array' => ['a', 'b']]);
         $attributes = $attributes->defaults($stimulusAttributes);
 
@@ -160,9 +158,7 @@ final class ComponentAttributesTest extends TestCase
             'data-action' => 'live#foo',
         ]);
 
-        $stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader(), [
-            'use_yield' => true,
-        ]));
+        $stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader()));
         $stimulusAttributes->addAction('foo', 'barMethod');
         $attributes = $attributes->defaults([...$stimulusAttributes]);