Why Mithril’s m.stream Makes State Management Simpler Than You Think
Mithril’s <code>m.stream</code> is a lightweight, zero‑dependency solution for reactive state. Learn how it works, see a counter example, and understand when it’s the right choice for your app.
08 Aug 2026, 03:31 UTC

Problem: Too Much Boilerplate for Simple Reactivity
When you start a new Mithril project you’ll quickly run into the need to keep UI state in sync with your data model. In larger frameworks like React you often pull in Redux or MobX to solve this, which adds a lot of boilerplate and bundle size. Mithril offers a built‑in, zero‑dependency solution called m.stream that lets you keep your UI reactive without a heavy state library.
Thesis: m.stream is the lightweight, high‑performance state tool you should use for most Mithril apps.
Unlike a plain JavaScript object, a stream is a function that can act as a getter or setter. When you call stream() with no arguments you get the current value; when you call it with an argument you set a new value and notify all subscribers. Under the hood Mithril’s virtual‑DOM diffing will only re‑render components that depend on the stream, keeping updates fast.
Section 1 – Understanding the Stream API
The API is intentionally tiny:
const counter = m.stream(0); // initial value
// Read the current value
console.log(counter()); // 0
// Update the value
counter(5);
Streams are also observable. You can subscribe to changes with stream((newVal, oldVal) => { … }). The callback runs immediately with the current value, then on every subsequent update. This makes streams a natural fit for component state and for exposing a reactive data source to other parts of your app.
Section 2 – A Worked Example: Counter with Streams
Below is a minimal counter component that uses m.stream for its state. The example shows how the component re‑renders only when the counter changes.
const Counter = {
view: function() {
return m('div', [
m('h1', 'Count: ' + Counter.value()),
m('button', {onclick: () => Counter.value(Counter.value() + 1)}, 'Increment'),
m('button', {onclick: () => Counter.value(Counter.value() - 1)}, 'Decrement')
]);
}
};
// Create a stream to hold the counter value
Counter.value = m.stream(0);
// Mount the component
m.mount(document.body, Counter);
Key points:
- The component itself is a plain object with a
viewmethod. - State lives in
Counter.value, a stream. - When the button callbacks call
Counter.value(newVal), Mithril automatically re‑rendersCounterbecause itsviewreads the stream. - No explicit
setStateoruseStatecalls are needed.
Because streams are tiny, the bundle size stays under 4 kB even with many counters. The diffing algorithm will compare the old virtual node to the new one and patch only the h1 element that displays the number.
Section 3 – Trade‑offs and Limitations
While m.stream shines for simple reactivity, it has a few caveats:
- Limited ecosystem. There are fewer third‑party libraries built around streams compared to Redux or RxJS. If you need advanced features like time‑travel debugging or middleware, you’ll have to implement them yourself.
- Learning curve. Developers used to JSX or template engines may find the hyperscript syntax and stream API unfamiliar at first.
- Not a full state container. Streams don’t provide actions, reducers, or a global store out of the box. For very large applications you might still want a dedicated state manager.
Despite these, for most small to medium‑sized applications the simplicity and performance of m.stream outweigh the downsides.
Actionable Closing: Try Streams in Your Next Project
To experiment, add the following to a new Mithril file:
// index.html
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script>
// Counter component from the example above
const Counter = {
view: function() {
return m('div', [
m('h1', 'Count: ' + Counter.value()),
m('button', {onclick: () => Counter.value(Counter.value() + 1)}, 'Increment'),
m('button', {onclick: () => Counter.value(Counter.value() - 1)}, 'Decrement')
]);
}
};
Counter.value = m.stream(0);
m.mount(document.body, Counter);
</script>
Open the page, open the browser console, and watch the counter update instantly. Try adding a second counter that shares the same stream to see how re‑rendering is scoped.
In summary, m.stream gives you reactive state with minimal code and negligible bundle impact. It’s a practical engineering decision for most Mithril projects that need fast, maintainable UI updates without pulling in a heavy state library.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.