Diagnosing Mithril Component Rendering Staleness
Learn why a Mithril component may render once and then stay stale, how to diagnose the cause, and what fixes to apply.
13 Aug 2026, 07:02 UTC

Recognizable condition
The user interface shows the initial render of a Mithril component, but after a user interaction, an m.request success, or a stream update the DOM does not change. Console logs reveal that the component’s state variable has a new value, yet the view output stays the same. No errors appear in the console and other Mithril components on the page continue to update normally.
Cause / diagnostic table
| Symptom | Likely cause |
|---|---|
| State changes but view not re‑invoked | Mutation happened outside Mithril’s auto‑redraw window (e.g., raw Promise.then, setTimeout, third‑party callback) |
| View logs show no new calls | Component returns an identical vnode reference on each call, causing the virtual‑DOM diff to bail out |
| View logs show calls but DOM unchanged | State was mutated in place (e.g., obj.prop = value) so the reference seen by the view did not change |
| No redraw scheduled after async work | Global m.redraw.strategy set to 'none' or manual redraw suppressed |
| Route param change does not trigger update | Same component instance reused because the route path matches and no key forces re‑init |
Ordered checks
- Confirm state actually changes – add
console.log('state before', state)before the mutation andconsole.log('state after', state)after; verify the values differ. - Verify view is invoked again – place
console.log('view render')at the top of theviewmethod and count logs after the interaction. - Inspect where mutation occurs – trace the code path; if it lies inside a
Promise,setTimeout, or a library callback, note that Mithril’s automatic redraw may not be scheduled. - Check vnode reference stability – in the
view, logreturn vnodeor stringify a part of the tree; if the logged value is identical across calls, the diff will bail out. - Check global redraw strategy – run
console.log(m.redraw.strategy); if it is'none'or you have calledm.redraw.sync(false)elsewhere, automatic redraws are disabled. - Route‑param reuse test – add
console.log(m.route.param('id'))inoninitand see whether it fires on param change; if not, the same instance is being reused.
Fixes tied to findings
State mutated in place
Replace in‑place mutation with an immutable update and explicitly request a redraw:
// before
obj.count = obj.count + 1;
// after
const newObj = { ...obj, count: obj.count + 1 };
obj = newObj; // re‑assign the reference
m.redraw(); // schedule a redraw if not already pending
If the state is a stream, use stream(value) to push a new value; streams auto‑trigger redraws.
Async callback outside auto‑redraw window
Wrap the mutation in m.redraw or use m.request which already schedules a redraw on success:
fetch('/api/data')
.then(r => r.json())
.then(data => {
// mutation here
myState = data;
m.redraw(); // explicit redraw
});
Alternatively, use m.request:
m.request({url: '/api/data'}).then(data => {
myState = data; // m.request auto‑redraws on success
});
Stable vnode reference
Ensure the view builds a fresh virtual DOM tree each call:
view(vnode) {
return m("div", [
m("h1", vnode.state.title),
m("button", {onclick: vnode.state.toggle}, "Toggle")
]);
}
Avoid returning a variable that holds a vnode from a previous render.
Redraw strategy suppressed
Restore the default strategy or call redraw manually after the state change:
// if you previously set
// m.redraw.strategy = 'none';
// reset it
m.redraw.strategy = null; // or 'diff' (default)
// or after mutation
m.redraw();
Route‑param reuse
Force a key change when the param changes, or read the param inside view so a new vnode is generated:
const MyComp = {
oninit: vnode => { vnode.state.id = m.route.param('id'); },
view: vnode => m("div", `ID: ${vnode.state.id}`)
};
// In routes
m.route.set('/item/:id', {id: 5});
// Changing the id will recreate the component because the path differs.
If you must keep the same path, add a key based on the param:
m.route.prefix = '';
m.route(document.body, '/', {
'/item/:id': {
render: vnode => m(MyComp, {key: vnode.attrs.id})
}
});
Escalation criteria
- DOM is manipulated directly by third‑party code, causing the virtual DOM and real DOM to fall out of sync.
- A library holds a reference to an old vnode (e.g., via a closure) and prevents garbage collection, leading to stale updates.
- Memory grows steadily after each interaction because event listeners or stream subscriptions are not removed in
onremove. - Repeated synchronous calls to
m.redraw.syncproduce a call‑stack overflow or noticeable frame‑rate drops, indicating an architectural coupling that requires a redesign of the update flow.
Limitations and practical verification
This guide addresses the classic component API (view, oninit, m.redraw). Functional components created with m helpers follow the same principles but may have different lifecycle hooks.
To verify that a fix works:
- Add a log at the start of
viewand confirm the log count increases after the user action. - Check that
m.redraw.pendingistrueimmediately after the state mutation (if you rely on automatic redraw). - Observe the DOM change in the browser; the updated text or attribute should appear without a page reload.
- If you used a stream, ensure you call
stream.end()or remove the listener inonremoveto prevent leaks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.