@bitstillery/mithril
Mithril.js with fine-grained reactivity, SSR hydration, and zero-dependency signals.
The Problem
Mithril.js uses global m.redraw()—one state change updates all components. This fork adds fine-grained reactivity: only components using changed state re-render.
What Makes This Different
Signals
Fine-grained reactivity primitives with automatic dependency tracking. Zero-dependency implementation—no Preact Signals or other packages.
import { signal, computed, effect } from '@bitstillery/mithril' const count = signal(0) const doubled = computed(() => count() * 2) effect(() => { console.log(`Count: ${count()}, Doubled: ${doubled()}`) }) count(5) // Logs: Count: 5, Doubled: 10
Signal State
Proxy-based reactive state that makes signals developer-friendly. Automatic dependency tracking with no manual redraw calls.
import { state } from '@bitstillery/mithril' const $s = state({ count: 0, user: { name: 'John' }, todos: [], totalTodos: () => $s.todos.length, // Computed }, 'my.state') // Name required for SSR serialization // Component only re-renders when $s.count changes class Counter extends MithrilTsxComponent { view() { return ( <div> <p>Count: {$s.count}</p> <button onclick={() => $s.count++}>Increment</button> </div> ) } }
Persistent Store
State persistence with automatic serialization. The Store class wraps state() with localStorage/sessionStorage support, seamlessly integrating with SSR hydration.
import { Store } from '@bitstillery/mithril' const store = new Store<{ user: { name: string }, preferences: Record<string, any> }>() // Define what persists vs what's volatile store.blueprint({ user: { name: '' }, preferences: {} }, { user: { name: '' }, // Persistent preferences: {}, // Persistent }) // Load from storage, or initialize with defaults store.load({ user: { name: 'John' }, preferences: { theme: 'dark' } }) // State is reactive and automatically saves on changes store.state.user.name = 'Jane' // Auto-saves to localStorage
SSR Hydration
Server-side rendering with state preservation. renderToString automatically serializes state; restore it on the client before mounting.
// Server const { html, state } = await m.renderToString(App) // Inject: <script id="__SSR_STATE__">${JSON.stringify(state)}</script> // Client import { deserializeAllStates } from '@bitstillery/mithril' const ssrState = document.getElementById('__SSR_STATE__') if (ssrState?.textContent) { deserializeAllStates(JSON.parse(ssrState.textContent)) } m.mount(root, App)
The Complete Picture
These features build on each other: signals provide the foundation for fine-grained reactivity, proxy-based state makes them developer-friendly, Store adds persistence for localStorage/sessionStorage, and SSR hydration enables search-engine friendly websites. The result? State that "just works"—from initial render through hydration, user interactions, and page refreshes—all while maintaining Mithril's familiar API.
Quick Start
bun add @bitstillery/mithril
import m, { state, MithrilTsxComponent } from '@bitstillery/mithril' const $s = state({ count: 0 }, 'app.state') // Name required for SSR class App extends MithrilTsxComponent { view() { return ( <div> <p>Count: {$s.count}</p> <button onclick={() => $s.count++}>Increment</button> </div> ) } } m.mount(document.body, App)
Examples
- SSR:
examples/ssr/- Server-side rendering with hydration - State:
examples/state/- Signals, state management, and Store persistence patterns
Differences from Mithril.js
| Feature | Original | This Fork |
|---|---|---|
| Reactivity | Global m.redraw() |
Fine-grained component updates |
| State Management | Manual redraw calls | Signals with automatic tracking |
| SSR Hydration | State loss | Proper state preservation |
| TypeScript | Community types | Native TypeScript |
100% API compatible with Mithril.js v2.x. Signals are opt-in.
Development
License
MIT License - see LICENSE file for details.
So Long, and Thanks for All the Fish
This is a fork of Mithril.js, built on the foundation laid by Leo Horie and the Mithril.js community. Special thanks to prominent contributors including Pat Cavit, Claudia Meadows, Zoli Kahan, Alec Embke, Barney Carroll, Dominic Gannaway, Boris Letocha, and Joel Richard.
This fork was built through a collaboration between AI and human direction: AI handled the implementation details, while human intent—documented in Architecture Decision Records—provided general direction and architectural choices. Signals came from good prior experience; SSR emerged naturally as we built. This isn't typical vibe coding—intent and details matter, even if the full impact isn't always clear upfront.
This implementation draws inspiration from:
- Preact Signals - Signal primitive, computed signals, dependency tracking
- DeepSignal - Deep reactivity patterns with Proxy
- Mithril PR #3036 - Component-level redraw pattern
- SolidJS - Fine-grained reactivity concepts