GitHub - bitstillery/mithril: Mithril with Signals, State & Store

3 min read Original article ↗

Mithril Bitstillery extends Mithril with integrated state management, SSR hydration, watchers, and a signal/proxy store. Drop-in compatible with Mithril v2.x.

bun add @bitstillery/mithril

Strengths

Mithril Bitstillery focuses on state management, SSR, watchers, and developer experience around its signal/proxy store:

Feature Description
Proxy State Reactive objects with nested objects, arrays, computeds
State Management state() + Store for persistence (localStorage, session)
SSR Full server-side rendering with state serialization + hydration
Watchers watch() for observing signal changes; effect() for side effects
DX Automatic dependency tracking, no manual redraw for signals

Docs: mithril.garage44.org

Signals

Zero-dependency reactive primitives with automatic dependency tracking:

import {signal, computed, effect} from '@bitstillery/mithril'

const count = signal(0)
const doubled = computed(() => count() * 2)

effect(() => console.log(`${count()} × 2 = ${doubled()}`))
count(5) // Logs: 5 × 2 = 10

Proxy State

state() creates reactive objects. Components track which properties they read and only re-render when those change. Function properties become computed values—they re-evaluate when their dependencies change.

import m, {state, MithrilComponent} from '@bitstillery/mithril'

const $s = state({count: 0, todos: [], totalTodos: () => $s.todos.length}, 'app')

class Counter extends MithrilComponent {
    view() {
        return (
            <div>
                <p>
                    {$s.count} / {$s.totalTodos}
                </p>
                <button onclick={() => $s.count++}>+</button>
            </div>
        )
    }
}

m.mount(document.body, Counter)
  • Computed properties: Any function in state (e.g. totalTodos: () => $s.todos.length) is a computed—read it like a normal property, it updates when dependencies change.
  • $ prefix: Use $s.$count for the raw signal (e.g. for watch()).
  • The second argument is a name used for SSR serialization.

Watchers

watch() observes signal changes:

import {state, watch} from '@bitstillery/mithril'

const $s = state({count: 0}, 'app')
const unwatch = watch($s.$count, (newVal, oldVal) => console.log(`${oldVal}${newVal}`))
$s.count++ // triggers callback
unwatch() // stop observing

SSR Hydration

// Server
const {html, state} = await m.renderToString(App)
// Inject: <script id="__SSR_STATE__">${JSON.stringify(state)}</script>

// Client
import {deserializeAllStates} from '@bitstillery/mithril'

const el = document.getElementById('__SSR_STATE__')
if (el?.textContent) deserializeAllStates(JSON.parse(el.textContent))
m.mount(root, App)

Persistent Store

Store wraps state() with localStorage/sessionStorage. Define a blueprint with defaults and which keys persist:

import {Store} from '@bitstillery/mithril'

const store = new Store<{user: {name: string}; preferences: Record<string, any>}>()
store.blueprint(
    {user: {name: ''}, preferences: {}},
    {user: {name: ''}, preferences: {}}, // Keys here persist to storage
)
store.load({user: {name: 'John'}, preferences: {theme: 'dark'}})
store.state.user.name = 'Jane' // Auto-saves

Examples

Development

License

MIT — see LICENSE.

Credits

Originally created by Leo Horie. See the Mithril.js contributors for the many people who made Mithril what it is.