What if reactive state was just... functions?
RuneHub is a lightweight, high-performance, and powerful reactive state library where the state is just a function that returns the initial value.
✨ Simple — No special constructors, no imports to create state.
const count = () => 0✨ Lazy evaluation — Initial values are not computed until first access.
const config = () => JSON.parse(localStorage.getItem('config'))
// Nothing executed yet — zero cost to define✨ Auto-naming — Function names are preserved automatically for debugging.
const count = () => 0
console.log(count.name) // "count" — automatic, zero runtime costCompare with other libraries — all require imports and manual debugger names:
SolidJS:
import { createSignal } from 'solid-js'
const [count, setCount] = createSignal(0, { name: "count" })Preact Signals:
import { signal } from '@preact/signals-core'
const count = signal(0, { name: "count" })MobX:
import { observable } from 'mobx'
const count = observable.box(0, { name: "count" })Jotai:
import { atom } from 'jotai'
const countAtom = atom(0)
if (process.env.NODE_ENV !== 'production') {
countAtom.debugLabel = 'count'
}RuneHub:
const count = () => 0- Zero initialization cost — Define hundreds of runes, pay only for what you use
- Lazy by design — Computes only when accessed and only when dependencies change
- Debug-friendly — Function names preserved automatically, no manual labeling or build plugins
- Simple API — One interface for state, computed, and effects
- No Proxy — Supports old browsers
- Fast — Competitive with the fastest reactive libraries (benchmarks)
- Tiny — 1.5 KB minzip full
- Zero dependencies — No external packages
- Automatic tracking — Dependencies and subscriptions managed for you
- Dynamic effects — Conditional logic automatically updates dependency graph
- Built-in batching — Multiple updates collapse into one notification
- Advanced event system — Fine-grained lifecycle events for precise control
- Isolated contexts — Create separate reactive scopes (hubs) when needed
- Type-safe — Full TypeScript support with type inference
- Framework-agnostic — Works anywhere JavaScript runs
[ Install ]
[ Usage ] Example Vanilla JS • Example React
[ Rune ] Types of runes • Dynamic dependencies • Runes are keys
[ Hub ] Why use custom hubs? • Use cases
[ Effects ] Basic effects • Nested effects
[ Events ] Event types • Unsubscribe
[ Slot ] Why use Slot directly? • Basic usage • Computed slots • Slot API • Custom slots
[ Hooks ] get • set • raw • on • off • update • destroy • batch • unwatch • slot • getSlot • hub
[ Common Pitfalls ] Dynamic runes • Mutating without update • Circular dependencies
[ TypeScript ] Type inference • Explicit return types • Explicit variable types • Type safety • DRY Principle
[ Performance ]
[ Links ]
Get started with RuneHub using your preferred package manager:
npm
npm i rune-hubyarn
yarn add rune-hubpnpm
pnpm add rune-hubOr use it directly in the browser via CDN:
<script src="https://cdn.jsdelivr.net/npm/rune-hub"></script>RuneHub works in any JavaScript environment — Node.js, browsers, Deno, Bun. No build tools required, though TypeScript is fully supported.
Import hooks from the package depending on your module system:
// ES modules
import { get, set } from 'rune-hub'
// CommonJS
const { get, set } = require('rune-hub')
// Browser (via CDN)
const { get, set } = RuneHubCreate state, computed values, effects and actions the same way — just functions:
import { get, set, on } from 'rune-hub'
// State
const count = () => 0
// Computed State
const double = () => get(count) * 2
// Side Effect
const log = () => console.log(get(double))
// Actions
const increase = () => set(count, get(count) + 1)A reactive counter with no build step or framework:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counter</title>
<script src="https://cdn.jsdelivr.net/npm/rune-hub"></script>
<script type="module">
const { get, set, on } = RuneHub
const count = () => 0
const increase = () => set(count, get(count) + 1)
const button = document.getElementById('counter')
button.addEventListener('click', increase)
on(() => {
button.innerText = `Count: ${get(count)}`
})
</script>
</head>
<body>
<button id="counter">Count: 0</button>
</body>
</html>A reactive counter in React:
import { set, get } from 'rune-hub'
import { useRune } from '@rune-hub/react'
const count = () => 0
const increase = () => set(count, get(count) + 1)
export function Counter() {
const value = useRune(count)
return (
<button onClick={increase}>
Count: {value}
</button>
)
}Types of runes • Dynamic dependencies • Runes are keys
A Rune is a function that takes no arguments. Functions that expect arguments are not runes.
type Rune<R = any> = () => RThe same shape () => value serves three roles:
1. State rune — holds a value:
const count = () => 0
const name = () => 'Alice'
const items = () => []2. Computed rune — derives from other runes:
const count = () => 0
const double = () => get(count) * 2
const quadruple = () => get(double) * 23. Effect rune — performs side effects:
const logger = () => console.log('Count:', get(count))
on(logger) // Runs when count changesThe role is determined by how you use the rune, not by which constructor you called.
Computed runes automatically track only the runes they actually read:
const showDetails = () => false
const name = () => 'Alice'
const age = () => 25
const display = () => {
const result = get(name)
if (get(showDetails)) {
return `${result}, ${get(age)}`
}
return result
}
const log = () => console.log(get(display))
on(log)
// logs: Alice
set(age, 26)
// nothing — age isn't tracked because showDetails is false
set(showDetails, true)
// logs: Alice, 26
set(age, 27)
// logs: Alice, 27
// now age is tracked!The dependency graph updates automatically based on which branches execute.
A rune doesn't hold state — it's a key to state. The actual value is stored in a hub. The same rune in different hubs has different values.
const count = () => 0
set(count, 5)
// Set 5 for the count in global hub
console.log(get(count))
// logs: 5
customHub.use(() => {
console.log(get(count))
// logs: 0 (initial value)
})Why use custom hubs? • Use cases
A Hub is a reactive context that manages runes. By default, RuneHub uses a global hub (Hub.root), but you can create isolated hubs for advanced use cases.
Custom hubs enable isolated reactive contexts where the same rune can have different values:
import { Hub, get, set } from 'rune-hub'
const count = () => 0
// Global hub
set(count, 5)
console.log(get(count)) // 5
// Custom hub
const customHub = new Hub()
customHub.use(() => {
console.log(get(count)) // 0 (fresh state!)
set(count, 100)
console.log(get(count)) // 100
})
// Back to global hub
console.log(get(count)) // still 5
// Back to custom hub
customHub.use(() => {
console.log(get(count)) // still 100
})1. Server-side rendering — Each request gets its own hub:
// Define runes once
const userId = () => ''
const userName = () => `User ${get(userId)}`
app.get('/user/:id', (req, res) => {
const requestHub = new Hub()
requestHub.use(() => {
// Same runes, isolated state per request
set(userId, req.params.id)
const html = renderApp() // uses userName rune
res.send(html)
})
})2. Testing — Each test gets a clean slate:
// Define runes once (shared across tests)
const count = () => 0
const double = () => get(count) * 2
test('counter increments', () => {
const testHub = new Hub()
testHub.use(() => {
set(count, 1)
expect(get(count)).toBe(1)
expect(get(double)).toBe(2)
})
})
test('counter starts at zero', () => {
const testHub = new Hub()
testHub.use(() => {
// Fresh state, not affected by previous test
expect(get(count)).toBe(0)
expect(get(double)).toBe(0)
})
})3. Temporary state — Try changes without affecting the main state:
const price = () => 100
const discount = () => 0
const total = () => get(price) - get(discount)
// Main app
console.log(get(total)) // 100
// Try different scenarios in isolated hub
const whatIfHub = new Hub()
whatIfHub.use(() => {
set(discount, 20)
console.log('With 20% discount:', get(total)) // 80
set(discount, 50)
console.log('With 50% discount:', get(total)) // 50
})
// Main state unchanged
console.log(get(total)) // still 100The use method returns the return value of the function passed to it.
const price = () => 100
const discount = () => 0
const total = () => get(price) - get(discount)
const whatIfHub = new Hub()
const whatIfTotal = whatIfHub.use(() => {
set(discount, 20)
return get(total)
})
console.log(whatIfTotal) // 80
console.log(get(total)) // still 100Basic effects • Nested effects
Effects are runes that perform side effects — logging, DOM updates, network requests, state mutations, or any operation beyond pure computation.
Activate an effect by passing it to on. The effect runs immediately and subscribes to any runes accessed inside:
const count = () => 0
const log = () => console.log('Count:', get(count))
on(log)
// logs: Count: 0
set(count, 5)
// logs: Count: 5Stopping an effect
on() returns a function to stop the effect:
const stop = on(log)
set(count, 10)
// logs: Count: 10
stop() // stop the effect
set(count, 15)
// nothing — effect stoppedMultiple effect activation
When calling on for an effect multiple times, you need to call all returned destructors to stop the effect:
const stop1 = on(log)
const stop2 = on(log)
stop1()
set(count, 10)
// logs: Count: 10
stop2()
set(count, 15)
// nothing — effect stoppedComplete effect stop
You can also use the off hook to force stop the effect:
on(log)
set(count, 10)
// logs: Count: 10
off(log) // stop the effect
set(count, 15)
// nothing — effect stoppedIf an effect runs inside another effect, it runs as a nested effect. Nested effects subscribe to their own dependencies independently — changes to nested effect dependencies don't trigger the parent effect. When a parent effect re-runs, nested effects are automatically cleaned up:
const count = () => 0
const message = () => ''
const logMessage = () => {
console.log('Message:', get(message))
}
const logCount = () => {
console.log('Count:', get(count))
on(logMessage)
}
on(logCount)
// logs: Count: 0
// logs: Message:
set(message, 'hello')
// logs: Message: hello
set(count, 1)
// logs: Count: 1
// logs: Message: hello
set(message, 'Hi')
// logs: Message: HiRuneHub provides a fine-grained event system for runes. Subscribe to specific lifecycle events using on(rune, event, listener).
| Event | Fired when |
|---|---|
init |
the rune has finished its first computation |
call |
the rune function has just been invoked |
update |
a value has been set (even if equal to the previous) |
change |
the value actually changed (prev !== cur) |
clear |
before recomputation — used for cleanup |
destroy |
the rune is being destroyed |
up |
the rune gained its first subscriber |
down |
the rune lost its last subscriber |
get |
fired when a rune's value is accessed |
const count = () => 0
on(count, 'init', () => console.log('initialized'))
on(count, 'change', () => console.log('changed to', raw(count)))
on(count, 'destroy', () => console.log('destroyed'))All event subscriptions return a function that removes the subscription:
const count = () => 0
// Subscribe to event
const stop = on(count, 'change', () => {
console.log('changed')
})
// Unsubscribe
stop()Using the off hook:
You can also use the off hook to remove event subscriptions:
const count = () => 0
const listener = () => console.log('changed')
// Subscribe
on(count, 'change', listener)
// Unsubscribe using off hook
off(count, 'change', listener)Why use Slot directly? • Basic usage • Computed slots • Slot API • Custom slots
Behind every rune is a Slot — the actual reactive container. Runes provide a functional API with lazy initialization and automatic naming, but you can work with slots directly for maximum performance or when you want an API similar to signals in other libraries.
Performance — Skip the slot lookup overhead (while Map lookup is O(1), direct slot access is faster as it skips the lookup entirely):
const count = () => 0
// With rune (Map lookup on every access)
get(count) // looks up slot in hub.slots Map, then accesses value
// Direct slot (no lookup)
const countSlot = new Slot(count)
countSlot.value // direct property accessFamiliar API — If you're coming from SolidJS, Preact Signals, or following the TC39 Signals Proposal:
// Similar to signals in other libraries and the TC39 proposal
const count = new Slot(() => 0)
// Property access
count.value = 5
console.log(count.value)
// Or method calls
count.set(5)
console.log(count.get())No Hub registration — Slots created with the global hub are anonymous by default (not registered in the hub's Map):
const count = () => 0 // rune
// Anonymous slot (default with global hub)
new Slot(count) // anon = true, not registered
// Registered slot (explicit)
new Slot(count, Hub.root, false) // anon = false, registered in Hub.root.slots
// Custom hub slots are registered by default
const customHub = new Hub()
new Slot(count, customHub) // anon = false, registered in customHub.slots
// Anonymous slots skip Map operations entirely for maximum performanceCreating a slot:
import { Slot } from 'rune-hub'
const count = new Slot(() => 0)The function provides the initial value. It's called lazily on first access.
Reading and writing:
count.value = 5
// or
count.set(5)
console.log(count.value) // 0
// or
console.log(count.get()) // 0Subscribing to changes:
count.on('change', () => {
console.log('Count:', count.raw)
})
count.set(10)
// logs: Count: 10Unsubscribing:
All event subscriptions return a function that removes the subscription:
// Subscribe to event
const stop = count.on('change', () => {
console.log('Count:', count.raw)
})
// Unsubscribe
stop()Using the off method:
You can also use the off method to remove event subscriptions:
const listener = () => console.log('Count:', count.raw)
count.on('change', listener) // start listening
count.off('change', listener) // stop listeningComputed slots automatically track dependencies:
const count = new Slot(() => 0)
const double = new Slot(() => count.value * 2)
double.on('change', () => {
console.log('Double:', double.value)
})
double.on() // to activate
count.value = 5
// logs: Double: 10Dependencies are tracked when you access .value or .get() inside the rune passed to the slot's constructor.
Conditional dependencies:
const showDetails = new Slot(() => false)
const name = new Slot(() => 'Alice')
const age = new Slot(() => 25)
const display = new Slot(() => {
const result = name.value
if (showDetails.value) {
return `${result}, ${age.value}`
}
return result
})
// Effect slot — performs side effects when dependencies change
const log = new Slot(() => {
console.log(display.value)
})
log.on() // Activate the effect
// logs: Alice
age.value = 26
// nothing — age not tracked
showDetails.value = true
// logs: Alice, 26
age.value = 27
// logs: Alice, 27 — now age is tracked
log.off()
// Stop loggingProperties:
slot.value // Get or set the current value
slot.cur // Current value (read-only, no subscription, no initialization)
slot.raw // Current value (read-only, no subscription)
slot.prev // Previous value (read-only)
slot.up // Is slot active (has subscribers or activated via up parameter)
slot.inited // Has slot been initialized
slot.deps // Set<Slot> — dependencies
slot.subs // Set<Slot> — subscribersMethods:
slot.on() // Activate slot for effects (returns destructor)
slot.on(event, listener) // Subscribe to specific event
slot.off() // Deactivate slot (remove from execution context)
slot.off(event, listener) // Unsubscribe from event
slot.update() // Force notification (when mutating objects/arrays)
slot.destroy() // Cleanup and remove all subscriptionsEvents:
Slots support the same event system as runes:
slot.on('change', () => {
console.log('Changed to:', slot.value)
})
slot.on('destroy', () => {
console.log('Slot destroyed')
})Extend Slot to add custom behavior:
import { Slot } from 'rune-hub'
class LoggedSlot<T> extends Slot<T> {
override set(value: T): void {
console.log(`[${this.rune.name}] ${this.cur} → ${value}`)
super.set(value)
}
}
const count = () => 0
const countSlot = new LoggedSlot(count)
countSlot.value = 5
// logs: [count] undefined → 5
countSlot.value = 10
// logs: [count] 5 → 10Debounced slot:
import { Slot, Hub } from 'rune-hub'
class DebouncedSlot<T> extends Slot<T> {
private timeout?: ReturnType<typeof setTimeout>
constructor(
rune: () => T,
private delay: number,
hub?: Hub,
anon?: boolean
) {
super(rune, hub, anon)
}
override set(value: T): void {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
super.set(value)
}, this.delay)
}
}
const search = new DebouncedSlot(() => '', 300)
const log = new Slot(() => {
console.log('Search:', search.value)
})
log.on()
search.value = 'a'
search.value = 'ab'
search.value = 'abc'
// logs: Search: abc (after 300ms, only once)get • set • raw • on • off • update • destroy • batch • unwatch • slot • getSlot • hub
Hooks are the functions you import from rune-hub to interact with runes.
They provide the API for reading, writing, subscribing, and managing reactive state.
Reads a rune's value and subscribes the current execution context to it.
function get<T extends Rune>(rune: T): ReturnType<T>const count = () => 0
on(() => console.log(get(count)))
// logs: 0
set(count, 1)
// logs: 1Updates a rune's value. Subscribers are notified only when the value actually changes (!==).
function set<T extends Rune>(rune: T, value: ReturnType<T>): voidconst count = () => 0
on(() => console.log(get(count)))
// logs: 0
set(count, 1) // logs: 1
set(count, 1) // nothing — value did not changeReads a rune's value without subscribing to it.
function raw<T extends Rune>(rune: T): ReturnType<T>const foo = () => 0
const bar = () => 0
const log = () => {
console.log(get(foo), raw(bar))
}
on(log)
// logs: 0, 0
set(foo, 1) // logs: 1, 0
set(bar, 1) // nothing — bar is read with raw
set(foo, 2) // logs: 2, 1Subscribes either to the rune itself (effect) or to a specific lifecycle event. Returns a Destructor that cancels the subscription.
function on (rune: Rune): Destructor
function on (rune: Rune, event: Event, listener: Listener, up?: boolean, free?: boolean): DestructorEffect form — invoke the function immediately and re-invoke whenever its dependencies change:
const count = () => 0
const log = () => {
console.log(get(count))
}
const off = on(log)
// logs: 0
set(count, 1) // logs: 1
off() // unsubscribe
set(count, 2) // nothingEvent form — listen to a specific event on the rune. Pass up = true to activate the slot eagerly so events like change start firing.
on(count, 'change', () => {
console.log('changed to', raw(count))
})Unsubscribes a listener or tears down the slot's whole subscription graph.
function off (rune: Rune): void
function off (rune: Rune, event: Event, listener: Listener): voidconst listener = () => console.log('changed')
on(count, 'change', listener)
off(count, 'change', listener)Forces subscribers to re-run, even if the rune's value reference is unchanged. Useful when mutating arrays, objects, or other values in place.
function update (rune: Rune): voidconst items = () => []
const log = () => console.log('items:', get(items))
on(log)
// logs: items: []
raw(items).push(1, 2, 3)
update(items)
// logs: items: [1, 2, 3]Destroys a rune's slot and removes it from the hub registry, clearing all dependencies, subscribers and event listeners.
function destroy (rune: Rune): voidon(count)
destroy(count) // full cleanupGroups multiple writes into a single notification cycle. Nested batch calls are flattened.
function batch (action: Action): voidconst a = () => 0
const b = () => 0
const sum = () => get(a) + get(b)
const log = () => console.log(get(sum))
on(log)
// logs: 0
batch(() => {
set(a, 400)
set(b, 20)
})
// logs: 420Runs a callback with dependency tracking disabled. get calls inside unwatch behave like raw.
function unwatch<A extends () => any>(action: A): ReturnType<A>const a = () => 0
const b = () => 0
const log = () => {
const av = get(a) // tracked
const bv = unwatch(() => get(b)) // not tracked
console.log(av, bv)
}
on(log)
// only re-runs when `a` changesReturns the slot for a rune in the current hub, creating it lazily if necessary.
function slot<T extends Rune>(rune: T): Slot<ReturnType<T>>Returns the slot for a rune only if it already exists, otherwise undefined. Does not allocate.
function getSlot<T extends Rune>(rune: T): Slot<ReturnType<T>> | undefinedReturns the currently active hub (Hub.cur), or Hub.root if no custom hub is in scope.
function hub (): HubDynamic runes • Mutating without update • Circular dependencies
Here are common mistakes to avoid when using RuneHub.
🏠︎ / Common Pitfalls / Dynamic runes ↓
❌ Don't create runes dynamically without cleanup:
// BAD: Creates new rune on every call
function processUser(id: number) {
const userName = () => `User ${id}` // New rune each time!
const log = () => {
console.log(get(userName))
}
on(log)
}
// Each call leaks a rune
processUser(1)
processUser(2)
processUser(3)Each call creates a new rune, but the old ones are never cleaned up. This causes memory leaks.
✅ Define runes once, update values:
// GOOD: Reuse the same rune
const userId = () => 0
const userName = () => `User ${get(userId)}`
const logUserName = () => {
console.log(get(userName))
}
on(logUserName)
function processUser(id: number) {
set(userId, id) // Update value, not rune
}
processUser(1) // logs: User 1
processUser(2) // logs: User 2✅ Or use cleanup if you must create dynamically:
// ACCEPTABLE: Clean up dynamic runes
function processUser(id: number) {
const userName = () => `User ${id}`
const log = () => {
console.log(get(userName))
}
on(log)
// Clean up when done
return () => {
destroy(log)
destroy(userName)
}
}
const cleanup = processUser(1)
// Later...
cleanup()✅ Or use a hub for isolated scope:
// BEST: Hub automatically cleans up everything
function processUser(id: number) {
const userHub = new Hub()
userHub.use(() => {
const userName = () => `User ${id}`
const log = () => {
console.log(get(userName))
}
on(log)
})
// Clean up when done
return () => {
userHub.destroy() // Cleans up all runes and effects at once
}
}
const cleanup = processUser(1)
// Later...
cleanup()🏠︎ / Common Pitfalls / Mutating without update ↑ ↓
❌ Don't mutate objects/arrays without notifying:
const items = () => []
const log = () => {
console.log('Items:', get(items))
}
on(log)
// BAD: Mutates but doesn't notify
raw(items).push(1, 2, 3)
// Effect doesn't re-run!✅ Call update after mutation:
const items = () => []
const log = () => {
console.log('Items:', get(items))
}
on(log)
// GOOD: Notify after mutation
raw(items).push(1, 2, 3)
update(items)
// Effect re-runs✅ Or use immutable updates:
const items = () => []
const log = () => {
console.log('Items:', get(items))
}
on(log)
// GOOD: Immutable update
set(items, [...get(items), 1, 2, 3])
// Effect re-runs🏠︎ / Common Pitfalls / Circular dependencies ↑
❌ Don't create circular dependencies:
// BAD: a depends on b, b depends on a
const a = () => get(b) + 1
const b = () => get(a) + 1
console.log(get(a)) // Stack overflow!✅ Restructure to avoid cycles:
// GOOD: Linear dependency chain
const base = () => 0
const a = () => get(base) + 1
const b = () => get(a) + 1
console.log(get(b)) // Works fine// ACCEPTABLE: Self-updating with termination condition
const count = () => 0
const increment = () => {
const value = get(count)
if (value < 3) {
set(count, value + 1) // Self-update
}
}
on(increment)
// Stops at 3Type inference • Explicit return types • Explicit variable types • Type safety • DRY Principle
RuneHub is written in TypeScript and provides full type inference with zero configuration. Types flow automatically from rune signatures through all hooks.
🏠︎ / TypeScript / Type inference ↓
Types are inferred directly from the rune's return value:
const count = () => 0
// Rune<number>
const name = () => 'Alice'
// Rune<string>
const user = () => ({ name: 'Alice', age: 25 })
// Rune<{ name: string; age: number }>
const items = () => [1, 2, 3]
// Rune<number[]>🏠︎ / TypeScript / Explicit return types ↑ ↓
Add explicit return types when inference isn't enough:
// Interface for complex objects
interface User {
name: string
age: number
email?: string
}
const user = (): User => ({
name: 'Alice',
age: 25,
})
// Union types
const status = (): 'idle' | 'loading' | 'success' | 'error' => 'idle'🏠︎ / TypeScript / Explicit variable types ↑ ↓
Specify the type directly on the variable when you need explicit control:
const count: Rune<number> = () => 0
const name: Rune<string> = () => 'Alice'
const items: Rune<number[]> = () => [1, 2, 3]🏠︎ / TypeScript / Type safety ↑ ↓
TypeScript catches errors at compile time:
const count = () => 0
const name = () => 'Alice'
// ✅ Type-safe operations
const value: number = get(count)
set(count, 5)
const doubled: number = get(count) * 2
// ❌ Type errors caught at compile time
set(count, 'string')
// Error: Argument of type 'string' is not assignable to parameter of type 'number'
const str: string = get(count)
// Error: Type 'number' is not assignable to type 'string'🏠︎ / TypeScript / DRY Principle ↑
RuneHub's architecture naturally follows the Don't Repeat Yourself (DRY) principle. When adding new state, you define it in one place — unlike many other state management libraries where you must duplicate definitions across interfaces and initial state.
The problem with other libraries:
In libraries like Zustand, TypeScript requires duplicating every state key in both the interface and the initial state:
// ❌ Zustand — duplication required
interface Store {
isOpen: boolean // Define here
open: () => void // Define here
close: () => void // Define here
toggle: () => void // Define here
}
const useNavbarStore = create<Store>((set, get) => ({
isOpen: false, // Duplicate here
open: () => set({ isOpen: true }), // Duplicate here
close: () => set({ isOpen: false }), // Duplicate here
toggle: () => set({ isOpen: !get().isOpen }), // Duplicate here
}))The interface is mandatory to get type-safe access via get(), but it forces you to maintain two sources of truth. Adding a new field means updating both the interface and the initial state.
RuneHub's solution:
With RuneHub, each piece of state is defined once as a function. TypeScript automatically infers the type from the function's return value:
// ✅ RuneHub — define once, use everywhere
const isOpen = () => false
const open = () => set(isOpen, true)
const close = () => set(isOpen, false)
const toggle = () => set(isOpen, !get(isOpen))Benefits:
- Single source of truth — Each state is defined in exactly one place
- Automatic type inference — TypeScript knows the type from the function
- Simpler code — No interfaces to maintain, no duplication
- Easier refactoring — Change the initial value in one place, types update automatically
- Better readability — Less boilerplate, clearer intent
rune-hub ships with benchmarks against watch-state, MobX, Effector, Nano Stores, Jotai, Zustand, valtio, and Redux.
Each library has different performance characteristics depending on the use case:
- State creation — Some libraries are faster at initializing new state containers
- Subscription creation — Some excel at adding new listeners/subscribers
- Read operations — Some optimize for frequent value access
- Write operations — Some optimize for frequent updates
RuneHub's performance bottleneck:
The main performance limitation in RuneHub comes from using Map and Set for dependency tracking and subscription management.
While these data structures provide O(1) algorithmic complexity for lookups and modifications, the actual runtime performance of Map/Set operations in current Node.js or Bun implementations is not yet optimized to match their theoretical efficiency.
This overhead affects operations like creating new reactive dependencies and managing subscriptions.
Despite this limitation, RuneHub achieves competitive benchmark results. As JavaScript runtimes continue to optimize Map and Set implementations, RuneHub's performance will improve automatically without requiring any code changes.
These benchmarks provide a rough performance comparison rather than a complete real-world picture. Some scenarios may be simplified — for example, they don't account for spread operations or the presence of other state in the store. The goal was to stress-test RuneHub under challenging conditions to identify and optimize bottlenecks during development.
Node.js v22.22.2
┌───────────────┬────────────┬──────────────────────┬───────┐
│ RuneHub: Rune │ 47578.8384 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ watch-state │ 32645.6803 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 68.6% │
│ RuneHub: Hub │ 32018.1653 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 67.3% │
│ Jotai │ 21765.475 │ ▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱ │ 45.7% │
│ Zustand │ 19339.2959 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 40.6% │
│ RuneHub: Slot │ 15526.7663 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 32.6% │
│ Redux │ 9049.4064 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 19% │
│ Redux: object │ 8482.9911 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 17.8% │
│ Nano Stores │ 3901.8599 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 8.2% │
│ Jotai: store │ 1733.722 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 3.6% │
│ MobX │ 1292.2632 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 2.7% │
│ valtio │ 620.39 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 1.3% │
│ Effector │ 163.2987 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 0.3% │
└───────────────┴────────────┴──────────────────────┴───────┘
Bun v1.3.11
┌───────────────┬────────────┬──────────────────────┬───────┐
│ RuneHub: Rune │ 33815.1414 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ watch-state │ 30594.1275 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 90.5% │
│ RuneHub: Hub │ 25768.9831 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 76.2% │
│ Zustand │ 19999.7185 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 59.1% │
│ RuneHub: Slot │ 19403.5672 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 57.4% │
│ Nano Stores │ 14472.2558 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 42.8% │
│ Jotai │ 13082.8 │ ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ │ 38.7% │
│ Redux: object │ 11735.6739 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 34.7% │
│ Redux │ 11663.4823 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 34.5% │
│ MobX │ 8797.0241 │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 26% │
│ valtio │ 2300.618 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 6.8% │
│ Effector │ 300.069 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 0.9% │
│ Jotai: store │ 269.6517 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 0.8% │
└───────────────┴────────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌──────────────────────┬────────────┬──────────────────────┬───────┐
│ Redux │ 30619.6374 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot event │ 26648.6429 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 87% │
│ Nano Stores │ 23608.4052 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 77.1% │
│ RuneHub: Rune event │ 21478.9286 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 70.1% │
│ Zustand │ 20828.6281 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 68% │
│ valtio │ 14258.8 │ ▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱ │ 46.6% │
│ watch-state │ 8967.4409 │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 29.3% │
│ RuneHub: Slot effect │ 3622.2143 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 11.8% │
│ Jotai │ 3047.6364 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 10% │
│ RuneHub: Rune effect │ 2262.1384 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 7.4% │
│ Effector │ 2101.9635 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 6.9% │
│ MobX: autorun │ 561.9025 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 1.8% │
│ MobX: reaction │ 400.1908 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 1.3% │
└──────────────────────┴────────────┴──────────────────────┴───────┘
Bun v1.3.11
┌──────────────────────┬───────────┬──────────────────────┬───────┐
│ Nano Stores │ 9183.2984 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ Redux │ 8860.2883 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 96.5% │
│ RuneHub: Slot event │ 8194.9091 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 89.2% │
│ RuneHub: Rune event │ 8093.3768 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 88.1% │
│ Zustand │ 6089.2229 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 66.3% │
│ valtio │ 4391.1902 │ ▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱ │ 47.8% │
│ watch-state │ 3364.0102 │ ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ │ 36.6% │
│ RuneHub: Slot effect │ 3081.5692 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 33.6% │
│ RuneHub: Rune effect │ 2234.1818 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 24.3% │
│ MobX: autorun │ 1284.0833 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 14% │
│ Jotai │ 1017.7626 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 11.1% │
│ MobX: reaction │ 1006.6298 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 11% │
│ Effector │ 985.8248 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 10.7% │
└──────────────────────┴───────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌─────────────────────┬────────────┬──────────────────────┬───────┐
│ watch-state: raw │ 49936.6869 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ Nano Stores: raw │ 49714.5354 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 99.6% │
│ watch-state: value │ 48560.6634 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 97.2% │
│ Zustand │ 48528.2178 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 97.2% │
│ RuneHub: Slot raw │ 48129.0099 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 96.4% │
│ Redux │ 47601.1863 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 95.3% │
│ RuneHub: Slot value │ 47274.3431 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 94.7% │
│ Effector │ 46851.5922 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 93.8% │
│ RuneHub: Rune raw │ 38024.2845 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 76.1% │
│ RuneHub: Rune get │ 36504.395 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 73.1% │
│ valtio │ 33801.4127 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 67.7% │
│ Nano Stores: get │ 15511.6703 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 31.1% │
│ Jotai │ 10074.9765 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 20.2% │
│ MobX │ 5165.6992 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 10.3% │
└─────────────────────┴────────────┴──────────────────────┴───────┘
Bun v1.3.11
┌─────────────────────┬────────────┬──────────────────────┬───────┐
│ watch-state: value │ 34859.2154 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ MobX │ 34808.8511 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 99.9% │
│ RuneHub: Slot raw │ 34794.9231 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 99.8% │
│ Effector │ 34163.4167 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 98% │
│ watch-state: raw │ 32991.7396 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 94.6% │
│ Zustand │ 32867.9855 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 94.3% │
│ RuneHub: Slot value │ 32684.09 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 93.8% │
│ Nano Stores: raw │ 32514.686 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 93.3% │
│ RuneHub: Rune raw │ 30014.1028 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 86.1% │
│ RuneHub: Rune get │ 29621.7103 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱ │ 85% │
│ Redux │ 28921.4615 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱ │ 83% │
│ valtio │ 28849.443 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱ │ 82.8% │
│ Nano Stores: get │ 15439.7288 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 44.3% │
│ Jotai │ 4828.5551 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 13.9% │
└─────────────────────┴────────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌─────────────────────┬────────────┬──────────────────────┬───────┐
│ RuneHub: Slot value │ 18176.0838 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot set │ 17576.8024 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 96.7% │
│ watch-state: value │ 16391.7797 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 90.2% │
│ watch-state: set │ 15787.2626 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 86.9% │
│ RuneHub: Rune │ 15530.0339 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 85.4% │
│ Zustand │ 15293.082 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱ │ 84.1% │
│ Nano Stores │ 10686.5433 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 58.8% │
│ Redux │ 7885.1171 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 43.4% │
│ valtio │ 4528.9157 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 24.9% │
│ Effector │ 3438.7595 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 18.9% │
│ Jotai │ 1700.4893 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 9.4% │
│ MobX │ 884.4583 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.9% │
└─────────────────────┴────────────┴──────────────────────┴───────┘
Bun v1.3.11
┌─────────────────────┬────────────┬──────────────────────┬───────┐
│ watch-state: set │ 20428.1951 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot set │ 20351.5682 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 99.6% │
│ RuneHub: Slot value │ 19733.2687 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 96.6% │
│ watch-state: value │ 18998.1667 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱ │ 93% │
│ Redux │ 15005.1325 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 73.5% │
│ RuneHub: Rune │ 14656.6329 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 71.7% │
│ Nano Stores │ 13000.0833 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱ │ 63.6% │
│ MobX │ 10599.72 │ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱ │ 51.9% │
│ Zustand │ 9736.9068 │ ▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱ │ 47.7% │
│ valtio │ 4333.0476 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 21.2% │
│ Effector │ 3774.3417 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 18.5% │
│ Jotai │ 912.6679 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.5% │
└─────────────────────┴────────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌──────────────────────┬───────────┬──────────────────────┬───────┐
│ RuneHub: Slot event │ 3702.1494 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot effect │ 2890.9552 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 78.1% │
│ RuneHub: Rune event │ 2885.3887 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 77.9% │
│ Nano Stores │ 2062.9236 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 55.7% │
│ RuneHub: Rune effect │ 2026.7029 │ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱ │ 54.7% │
│ Zustand │ 1825.9319 │ ▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱ │ 49.3% │
│ Redux │ 1430.788 │ ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ │ 38.6% │
│ watch-state │ 1089.6411 │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 29.4% │
│ valtio │ 444.4082 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 12% │
│ Effector │ 286.7872 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 7.7% │
│ Jotai │ 170.2626 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.6% │
│ MobX │ 77.2408 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 2.1% │
└──────────────────────┴───────────┴──────────────────────┴───────┘
Bun v1.3.11
┌──────────────────────┬───────────┬──────────────────────┬───────┐
│ RuneHub: Slot event │ 6176.7243 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Rune event │ 4754.693 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 77% │
│ RuneHub: Slot effect │ 3992.2652 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱ │ 64.6% │
│ Nano Stores │ 3683.5105 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 59.6% │
│ RuneHub: Rune effect │ 3311.2642 │ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱ │ 53.6% │
│ Redux │ 2712.2913 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 43.9% │
│ Zustand │ 1420.4286 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 23% │
│ MobX │ 1148.8989 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 18.6% │
│ watch-state │ 1018.9713 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 16.5% │
│ valtio │ 440.2069 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 7.1% │
│ Effector │ 280.3208 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.5% │
│ Jotai │ 85.7651 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 1.4% │
└──────────────────────┴───────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌──────────────────────┬───────────┬──────────────────────┬───────┐
│ Zustand │ 2425.9228 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot event │ 2001.3538 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱ │ 82.5% │
│ RuneHub: Rune event │ 1362.7011 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 56.2% │
│ Nano Stores │ 1326.894 │ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱ │ 54.7% │
│ RuneHub: Slot effect │ 1046.2021 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 43.1% │
│ watch-state │ 1008.6272 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 41.6% │
│ RuneHub: Rune effect │ 863.6632 │ ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ │ 35.6% │
│ Redux │ 766.6586 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 31.6% │
│ valtio │ 466.6553 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 19.2% │
│ Jotai │ 213.0101 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 8.8% │
│ Effector │ 79.58 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 3.3% │
│ MobX │ 74.1477 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 3.1% │
└──────────────────────┴───────────┴──────────────────────┴───────┘
Bun v1.3.11
┌──────────────────────┬───────────┬──────────────────────┬───────┐
│ Nano Stores │ 3283.7276 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot event │ 2383.3333 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 72.6% │
│ Zustand │ 2139.7824 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 65.2% │
│ RuneHub: Rune event │ 1867.2293 │ ▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │ 56.9% │
│ RuneHub: Slot effect │ 1314.0722 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 40% │
│ RuneHub: Rune effect │ 1127.4746 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 34.3% │
│ watch-state │ 1104.587 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 33.6% │
│ Redux │ 865.4946 │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 26.4% │
│ valtio │ 739.6796 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 22.5% │
│ MobX │ 595.9964 │ ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 18.1% │
│ Effector │ 146.6858 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.5% │
│ Jotai │ 105.1453 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 3.2% │
└──────────────────────┴───────────┴──────────────────────┴───────┘
Node.js v22.22.2
┌──────────────────────┬────────┬──────────────────────┬───────┐
│ Zustand │ 1.0733 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Slot event │ 0.9367 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱ │ 87.3% │
│ Nano Stores │ 0.7633 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ │ 71.1% │
│ RuneHub: Rune event │ 0.74 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 68.9% │
│ valtio │ 0.4767 │ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱ │ 44.4% │
│ Redux │ 0.3633 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 33.9% │
│ Effector │ 0.1405 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 13.1% │
│ Jotai │ 0.1325 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 12.3% │
│ RuneHub: Slot effect │ 0.1279 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 11.9% │
│ RuneHub: Rune effect │ 0.1262 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 11.8% │
│ watch-state │ 0.0717 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 6.7% │
│ MobX │ 0.0138 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 1.3% │
└──────────────────────┴────────┴──────────────────────┴───────┘
Bun v1.3.11
┌──────────────────────┬────────┬──────────────────────┬───────┐
│ RuneHub: Slot event │ 1.7633 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ │ 100% │
│ RuneHub: Rune event │ 1.34 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱ │ 76% │
│ Zustand │ 1.1827 │ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱ │ 67.1% │
│ Nano Stores │ 0.96 │ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱ │ 54.4% │
│ Redux │ 0.5833 │ ▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 33.1% │
│ valtio │ 0.44 │ ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 25% │
│ Effector │ 0.2259 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 12.8% │
│ MobX │ 0.2167 │ ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 12.3% │
│ RuneHub: Rune effect │ 0.1667 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 9.5% │
│ RuneHub: Slot effect │ 0.1513 │ ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 8.6% │
│ Jotai │ 0.0839 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 4.8% │
│ watch-state │ 0.0682 │ ▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ │ 3.9% │
└──────────────────────┴────────┴──────────────────────┴───────┘
To run the benchmarks on your own machine, clone the repository, install dependencies, and execute the test commands:
# Clone the repository
git clone https://github.com/d8corp/rune-hub.git
cd rune-hub
# Install dependencies
npm i
# Run the full benchmark suite
npm run speed:node # Node.js
npm run speed:bun # BunOr run focused scenarios:
# Node.js
npm run speed:node:init # initialization
npm run speed:node:get # reads
npm run speed:node:set # writes
npm run speed:node:batching # batched updates
npm run speed:node:examples # end-to-end scenarios
# Bun
npm run speed:bun:init # initialization
npm run speed:bun:get # reads
npm run speed:bun:set # writes
npm run speed:bun:batching # batched updates
npm run speed:bun:examples # end-to-end scenarios- Creator: Mike Lysikov
- Source Code: GitHub
- Repository: npm • npmx
- Progenitor: watch-state
- Frameworks: @rune-hub/react
- Utils: @rune-hub/utils
Contributions are welcome! Please feel free to submit issues and pull requests.