Solving Stale UI: Diagnosing Missing Redraws in Mithril.js
Learn how to diagnose and fix stale UI issues in Mithril.js caused by missing redraw cycles in external callbacks, timers, and WebSockets.
27 Jul 2025, 12:58 UTC

The Problem: State Updates Without UI Changes
In Mithril.js, you may encounter a scenario where your JavaScript state (the data) updates correctly, but the browser DOM remains unchanged. This typically happens when state mutations occur outside of Mithril's managed event loop, leaving the framework unaware that it needs to perform a virtual DOM diff and update the screen.
Diagnostic Matrix: Why the UI is Stale
Use this table to identify if your issue is a missing redraw cycle or a logic error in your component.
| Symptom | State Value (Console) | DOM State | Likely Cause |
|---|---|---|---|
| UI doesn't update after API response | Updated/Correct | Old/Stale | External callback bypasses auto-redraw |
| Timer/Clock doesn't tick | Updated/Correct | Old/Stale | setInterval/setTimeout bypasses loop |
| UI doesn't update on click | Old/Incorrect | Old/Stale | Event handler logic error or state mutation failure |
| UI updates, then reverts | Correct | Flickering | Race condition or conflicting redraw calls |
Step-by-Step Diagnostic Process
- Verify State Mutation: Place a
console.log()immediately after the line of code that modifies your state. If the log shows the new value but the screen is static, the problem is the redraw cycle, not your data logic. - Identify the Trigger Source: Determine what triggered the change. Mithril automatically triggers a redraw for events it manages (e.g.,
onclick,oninput). If the trigger is aWebSocketmessage, asetTimeout, or a third-party library callback, Mithril will not auto-redraw. - Check Lifecycle Placement: Ensure you aren't attempting to update state in a way that conflicts with the current rendering phase. Updates inside a render function are ignored or can cause instability.
Fixes Based on Findings
Scenario A: External Callbacks or Timers
When using setInterval or an external API callback, you must manually notify Mithril to synchronize the DOM.
// Example: A timer that updates a counter
let state = { count: 0 };
setInterval(() => {
state.count++;
// The state changes, but the UI stays the same without this call:
m.redraw();
}, 1000);
Scenario B: Refactoring to Managed Events
To avoid scattering m.redraw() calls throughout your codebase, wrap external updates in a handler that ensures a redraw is scheduled. This keeps your business logic separate from the rendering trigger.
Implementation Risks and Constraints
Manual redraws are powerful but can introduce performance bottlenecks or application crashes if misused.
- Infinite Loop Risk: Never call
m.redraw()inside a render function or a lifecycle hook that is triggered by a redraw (likeoncreateoronupdate). This creates a recursive loop that will freeze the browser tab. - Performance Degradation: Avoid calling
m.redraw()inside high-frequency events such asonmousemoveoronscroll. Constant virtual DOM diffing can lead to significant input lag.
Verification and Testing
To verify the fix, follow these steps:
- Open Browser Developer Tools (F12) and navigate to the Elements tab.
- Trigger the state change (e.g., wait for the timer or trigger the API call).
- Observe the DOM elements. If the redraw is working, you will see the specific HTML nodes flash or update in the inspector.
- Check the Console to ensure no "Maximum call stack size exceeded" errors appear, which would indicate an infinite redraw loop.
Rollback Procedure
If adding m.redraw() causes performance drops or crashes, remove the manual call and move the state mutation into a Mithril-managed event handler (e.g., moving a manual trigger into an onclick handler) to restore the default automatic redraw behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.