Mithril State Management: Choosing Between Local Component State and Shared Stores
Choose between local component properties and shared store objects in Mithril.js to maintain UI synchronization without sacrificing performance.
07 Aug 2025, 07:43 UTC

The State Synchronization Challenge
In Mithril.js, the key decision is where data should live to maintain UI synchronization without triggering excessive redraws. Since Mithril automatically refreshes the DOM after most event handlers, developers must choose between encapsulating state within a component or sharing it globally.
Core takeaway: Use local component properties for transient UI state and shared JavaScript objects for application-wide data synchronization.
State Strategy Comparison
Mithril has no built-in state management library like Redux. The following table compares the two primary patterns:
| Feature | Local Component State | Shared Store Object |
|---|---|---|
| Scope | Private to component instance | Accessible by any component |
| Persistence | Destroyed on unmount | Persists for app lifecycle |
| Update Trigger | Automatic via event handlers | Automatic or via m.redraw() |
| Complexity | Low; uses this or closures |
Medium; requires separate module |
Trade-offs and Decision Logic
Local State Limitations
When state is attached to the component object (e.g., this.isOpen = false), it remains encapsulated. This works well for "UI-only" state. However, if sibling components need access, you must lift state to a common parent or move it to a shared store. Relying solely on local state in complex apps leads to "prop drilling" where data passes through unnecessary component layers.
Shared Store Limitations
A shared store is a JavaScript object exported from a module. While this solves synchronization, it introduces "out-of-band updates" risk. If the store changes via setTimeout, WebSocket callbacks, or third-party libraries, Mithril's auto-redraw won't detect it. Call m.redraw() manually in these cases.
Implementation Example: Hybrid Approach
This example shows a shared store for user profile data and local state for a "Editing" toggle. Tested with Mithril v2.x.
// store.js - Shared Store
export const UserStore = {
name: "Guest",
email: "[contact removed]",
updateName(newName) {
this.name = newName;
}
};
// ProfileComponent.js
import { UserStore } from './store.js';
const Profile = {
isEditing: false,
view() {
return m("div", [
m("h1", `Welcome, ${UserStore.name}`),
m("button", {
onclick: () => {
this.isEditing = !this.isEditing;
}
}, this.isEditing ? "Close Edit" : "Edit Profile"),
this.isEditing && m("div", [
m("input", {
oninput: (e) => {
UserStore.updateName(e.target.value);
},
value: UserStore.name
})
])
]);
}
};
Verification Steps
- Local State Check: Navigate away from and back to the component. The
isEditingtoggle should reset tofalse. - Store Sync Check: Create a second component displaying
UserStore.name. Change the name in Profile; the second component should update immediately. - Manual Redraw Test: Update
UserStore.nameinside asetInterval. The UI won't change until you addm.redraw()inside the interval.
Performance Considerations
Frequent m.redraw() calls—such as on mouse-move events—can degrade performance by re-evaluating the virtual DOM tree. For high-frequency updates, use m.redraw sparingly or update specific DOM elements directly via references when the change doesn't affect overall app state.
Rollback Strategy
Since this pattern uses standard JS objects, "rolling back" requires implementing a history stack or snapshot system within your store (e.g., saving previous state to a lastKnownState variable before mutation).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.