Skip to content

Instantly share code, notes, and snippets.

@marmay
Last active January 24, 2026 18:00
Show Gist options
  • Select an option

  • Save marmay/13854bba177d853545a038145bdc84e4 to your computer and use it in GitHub Desktop.

Select an option

Save marmay/13854bba177d853545a038145bdc84e4 to your computer and use it in GitHub Desktop.
miso-bug-report-2
commit 696f38022202ccdca597b439988144560538032c
Author: Markus Mayr <[email protected]>
Date: Sat Jan 24 18:17:18 2026 +0100
Fix component lifecycle bugs causing leaks and event issues
Four fixes for component lifecycle management:
1. Copy componentId when VComp keys match (dom.ts)
When diffing two VComps with matching keys, the componentId was not
being copied from the old tree to the new tree. This caused unmount
callbacks to receive undefined componentId, preventing proper cleanup.
2. Use destroy() instead of removeChild() in syncChildren (dom.ts)
When removing excess old children in the keyed children sync algorithm,
removeChild() was used which only removes the DOM element. This should
be destroy() to properly trigger unmount callbacks and cleanup.
3. Update parent VComp.child reference after component re-render (Runtime.hs)
After a component re-renders, the parent VComp's .child reference
still pointed to the old VTree. This caused event delegation to fail
because it traverses the VTree looking for event handlers, but the
stale reference meant new handlers weren't found.
4. Add base case for empty queue in drain function (Runtime.hs)
The drain function recursively processes the action queue but had no
base case for when the queue is empty, causing infinite recursion.
diff --git a/src/Miso/Runtime.hs b/src/Miso/Runtime.hs
index 8933582..bcf9c98 100644
--- a/src/Miso/Runtime.hs
+++ b/src/Miso/Runtime.hs
@@ -185,6 +185,7 @@ initialize events _componentParentId hydrate isRoot comp@Component {..} getCompo
_timestamp :: Double <- takeMVar frame
Diff.diff (Just oldVTree) (Just newVTree) _componentDOMRef
liftIO (atomicWriteIORef _componentVTree newVTree)
+ FFI.updateRef oldVTree newVTree
let _componentApplyActions = \actions model_ -> do
let info = ComponentInfo _componentId _componentParentId _componentDOMRef
@@ -915,6 +916,7 @@ drain
-> IO ()
drain cs@ComponentState {..} = do
drainQueueAt _componentId >>= \case
+ [] -> pure () -- Base case: queue empty, stop recursion
actions -> do
case _componentApplyActions actions _componentModel of
(updated, schedules :: [Schedule action]) -> do
diff --git a/ts/miso/dom.ts b/ts/miso/dom.ts
index c5c2be2..69dd130 100644
--- a/ts/miso/dom.ts
+++ b/ts/miso/dom.ts
@@ -11,6 +11,7 @@ export function diff<T>(c: VTree<T>, n: VTree<T>, parent: T, context: DrawingCon
else if (c.type === VTreeType.VComp && n.type === VTreeType.VComp) {
if (n.key === c.key) {
n.child = c.child;
+ n.componentId = c.componentId;
if (c.child) c.child.parent = n;
return;
}
@@ -430,7 +431,7 @@ function syncChildren<T>(os: Array<VTree<T>>, ns: Array<VTree<T>>, parent: T, co
else if (newFirstIndex > newLastIndex) {
tmp = oldLastIndex;
while (oldLastIndex >= oldFirstIndex) {
- removeChild(parent, os[oldLastIndex--], context);
+ destroy(os[oldLastIndex--], parent, context);
}
os.splice(oldFirstIndex, tmp - oldFirstIndex + 1);
break;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment