When building complex UIs with dockable panels, modal dialogs, or drag-and-drop interfaces, you often need to move components between different DOM parents. In Mithril.js, this creates a problem: moving a component to a different parent destroys the component instance and loses all local state, even when using the same key
.
The Problem
// Before: component in sidebar
m('div.sidebar', [
m(MyComponent, { key: 'editor-1' }) // Has local state
]);
// After: component moved to floating window
m('div.floating-window', [
m(MyComponent, { key: 'editor-1' }) // NEW instance, lost state!
]);
Even with identical keys, Mithril treats this as component removal + creation because the parent changed. The key
attribute only helps with reordering within the same parent.
The Solution: Reference-Counted State Store
The key insight is that during reparenting, both the old and new component instances with the same key briefly exist simultaneously. We can use this overlap to detect reparenting vs. true removal.
How It Works
During reparenting (same key, different parent):
- Old component instance exists →
refCount = 1
- New component instance created with same key →
refCount = 2
- Old component instance removed →
refCount = 1
, state preserved - New component instance continues with restored state
During true removal:
- Component instance exists →
refCount = 1
- Component instance removed, no replacement →
refCount = 0
, state cleaned up
Example: Persistent Counter
class Counter {
static stateStore = new Map();
oninit(vnode) {
const key = vnode.key;
// Get or create state entry
let entry = Counter.stateStore.get(key);
if (!entry) {
entry = {
refCount: 0,
state: { count: 0 }
};
Counter.stateStore.set(key, entry);
}
entry.refCount++;
this.persistentState = entry.state;
}
onbeforeremove(vnode) {
const key = vnode.key;
const entry = Counter.stateStore.get(key);
if (entry) {
entry.refCount--;
if (entry.refCount === 0) {
Counter.stateStore.delete(key);
}
}
}
view() {
return m('div', [
m('p', `Count: ${this.persistentState.count}`),
m('button', {
onclick: () => this.persistentState.count++
}, 'Increment')
]);
}
}
// Usage - counter value survives reparenting between different containers
m(Counter, { key: 'my-counter' });
Benefits
-
Automatic: Works with existing
key
attributes - Memory Safe: Reference counting prevents leaks
- Framework Native: Uses only Mithril's lifecycle hooks
- Simple: Just add the pattern to any component that needs it
Conclusion
This pattern fills a gap in Mithril's component lifecycle, enabling sophisticated UI behaviors like undockable panels and persistent modal dialogs. The reference counting approach is elegant because it leverages Mithril's own timing - the brief moment when both the old vnode and the new vnode exist during reparenting becomes our signal to preserve state.
Top comments (0)