@varbyte/boxstore
v0.1.0
Published
Lightweight, framework-agnostic state management library combining signals reactivity with Redux patterns
Maintainers
Readme
@varbyte/boxstore
Lightweight, framework-agnostic state management library combining signals reactivity with Redux patterns
A hybrid state management solution built on @varbyte/signals-core that provides the reactivity of signals with the predictability of Redux actions and middleware.
Features
- ✅ Signals-based reactivity - Zero-overhead reactivity from @varbyte/signals-core
- ✅ Redux-like actions - Predictable state mutations through explicit actions
- ✅ Middleware system - Extensible with logger, persist, devtools, and custom middleware
- ✅ TypeScript strict mode - Full type inference for state and actions
- ✅ Framework-agnostic - Works with any framework or vanilla JS
- ✅ Tiny bundle - Core < 3KB gzipped (excluding middleware)
- ✅ Async actions - First-class support for async operations
Installation
npm install @varbyte/boxstore @varbyte/signals-coreOr with your preferred package manager:
pnpm add @varbyte/boxstore @varbyte/signals-core
yarn add @varbyte/boxstore @varbyte/signals-core
bun add @varbyte/boxstore @varbyte/signals-coreQuick Start
import { createStore } from '@varbyte/boxstore'
import { computed, effect } from '@varbyte/signals-core'
// Create a store with state and actions
const store = createStore({
state: {
count: 0
},
actions: {
increment() {
this.state.count.update(n => n + 1)
},
decrement() {
this.state.count.update(n => n - 1)
},
reset() {
this.state.count.set(0)
}
}
})
// Use actions to mutate state
store.increment()
console.log(store.state.count()) // 1
// Create computed values
const doubled = computed(() => store.state.count() * 2)
console.log(doubled()) // 2
// React to state changes
effect(() => {
console.log(`Count: ${store.state.count()}`)
})
// Logs: Count: 1
store.increment()
// Logs: Count: 2Core Concepts
State as Signals
State is automatically converted to signals, enabling fine-grained reactivity:
const store = createStore({
state: {
user: null,
todos: []
},
actions: {}
})
// Each state property is a signal
store.state.user() // Read value
store.state.user.set({ id: 1, name: 'Jane' }) // Set value
store.state.todos.update(todos => [...todos, newTodo]) // Update based on currentActions
Actions are methods with access to this.state that can mutate state:
const store = createStore({
state: { count: 0 },
actions: {
// Synchronous actions
increment() {
this.state.count.update(n => n + 1)
},
// Async actions
async fetchUser(id: number) {
const user = await api.getUser(id)
this.state.user.set(user)
}
}
})
store.increment() // Call actions directly
await store.fetchUser(123)Middleware
Middleware intercepts action calls (not direct signal mutations):
import { createStore } from '@varbyte/boxstore'
import { logger, persist } from '@varbyte/boxstore/middleware'
const store = createStore({
state: { count: 0 },
actions: {
increment() {
this.state.count.update(n => n + 1)
}
},
middleware: [
logger({ collapsed: true }),
persist({ key: 'my-app', storage: localStorage })
]
})
// Action call triggers middleware
store.increment() // → logger logs, persist saves
// Direct signal mutation bypasses middleware
store.state.count.set(5) // → no middleware calledMiddleware
Logger
Console logging middleware for development:
import { logger } from '@varbyte/boxstore/middleware'
const store = createStore({
state: { count: 0 },
actions: { increment() { this.state.count.update(n => n + 1) } },
middleware: [
logger({ collapsed: true }) // Use collapsed groups
]
})
store.increment()
// Console:
// ▶ action increment
// prev state: { count: 0 }
// next state: { count: 1 }Persist
Storage sync middleware with hydration:
import { persist } from '@varbyte/boxstore/middleware'
const store = createStore({
state: { count: 0 },
actions: { increment() { this.state.count.update(n => n + 1) } },
middleware: [
persist({
key: 'my-app',
storage: localStorage // or sessionStorage
})
]
})
// Hydrates from localStorage on creation
// Saves to localStorage after each actionDevTools
Redux DevTools integration:
import { devtools } from '@varbyte/boxstore/middleware'
const store = createStore({
state: { count: 0 },
actions: { increment() { this.state.count.update(n => n + 1) } },
middleware: [devtools()]
})
// Open Redux DevTools extension to see:
// - Action history
// - State snapshots
// - Time-travel debuggingExamples
Counter
import { createStore } from '@varbyte/boxstore'
import { effect } from '@varbyte/signals-core'
const store = createStore({
state: { count: 0 },
actions: {
increment() {
this.state.count.update(n => n + 1)
},
decrement() {
this.state.count.update(n => n - 1)
}
}
})
effect(() => {
document.getElementById('count')!.textContent = String(store.state.count())
})
document.getElementById('inc')!.onclick = () => store.increment()
document.getElementById('dec')!.onclick = () => store.decrement()Todo List
import { createStore } from '@varbyte/boxstore'
import { computed } from '@varbyte/signals-core'
import { logger, persist } from '@varbyte/boxstore/middleware'
interface Todo {
id: number
text: string
done: boolean
}
const store = createStore({
state: {
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed'
},
actions: {
addTodo(text: string) {
const todo: Todo = { id: Date.now(), text, done: false }
this.state.todos.update(todos => [...todos, todo])
},
toggleTodo(id: number) {
this.state.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
)
},
removeTodo(id: number) {
this.state.todos.update(todos => todos.filter(t => t.id !== id))
},
setFilter(filter: 'all' | 'active' | 'completed') {
this.state.filter.set(filter)
}
},
middleware: [
logger({ collapsed: true }),
persist({ key: 'todos', storage: localStorage })
]
})
// Computed filtered list
const filteredTodos = computed(() => {
const todos = store.state.todos()
const filter = store.state.filter()
if (filter === 'active') return todos.filter(t => !t.done)
if (filter === 'completed') return todos.filter(t => t.done)
return todos
})
// Computed stats
const activeCount = computed(() =>
store.state.todos().filter(t => !t.done).length
)TypeScript
Full type inference for state and actions:
const store = createStore({
state: {
count: 0,
user: null as { id: number; name: string } | null
},
actions: {
setUser(user: { id: number; name: string }) {
this.state.user.set(user)
}
}
})
// TypeScript infers:
store.state.count // WritableSignal<number>
store.state.user // WritableSignal<{ id: number; name: string } | null>
store.setUser({ id: 1, name: 'Jane' }) // ✅ Type-safe
store.setUser({ id: 1 }) // ❌ TypeScript error: missing 'name'Framework Adapters
While Boxstore core is framework-agnostic, we provide official adapters that make it seamless to use with popular frameworks:
React
React hooks for reactive state management with automatic re-renders.
Installation:
npm install @varbyte/boxstore-reactUsage:
import { createStore } from '@varbyte/boxstore';
import { useSelector } from '@varbyte/boxstore-react';
const counterStore = createStore({
state: { count: 0 },
actions: {
increment() {
this.state.count.update(n => n + 1);
}
}
});
function Counter() {
const count = useSelector(counterStore, s => s.state.count());
return (
<button onClick={() => counterStore.increment()}>
Count: {count}
</button>
);
}Preact
Preact hooks with the same API as React, optimized for Preact's smaller runtime.
Installation:
npm install @varbyte/boxstore-preactUsage:
import { createStore } from '@varbyte/boxstore';
import { useSelector } from '@varbyte/boxstore-preact';
const counterStore = createStore({
state: { count: 0 },
actions: {
increment() {
this.state.count.update(n => n + 1);
}
}
});
function Counter() {
const count = useSelector(counterStore, s => s.state.count());
return (
<button onClick={() => counterStore.increment()}>
Count: {count}
</button>
);
}Astro
Seamless state sharing across Astro islands with React and Preact support.
Installation:
npm install @varbyte/boxstore-astroUsage:
// src/store.ts
import { createStore } from '@varbyte/boxstore';
export const cartStore = createStore({
state: { items: [] as string[] },
actions: {
addItem(item: string) {
this.state.items.update(items => [...items, item]);
}
}
});
// src/components/CartButton.tsx
import { useSelector } from '@varbyte/boxstore-astro/react';
import { cartStore } from '../store';
export function CartButton() {
const itemCount = useSelector(cartStore, s => s.state.items().length);
return <button>Cart ({itemCount})</button>;
}
// src/pages/index.astro
---
import { CartButton } from '../components/CartButton';
---
<CartButton client:load />Bundle Sizes
| Package | Size (gzipped) | |---------|----------------| | @varbyte/boxstore | < 3KB | | @varbyte/boxstore-react | < 2KB | | @varbyte/boxstore-preact | < 1.5KB | | @varbyte/boxstore-astro | < 2.5KB |
API Reference
createStore(config)
Creates a reactive store with signals-based state and actions.
Parameters:
config.state- Plain object with initial state valuesconfig.actions- Object with action methods (have access tothis.state)config.middleware?- Optional array of middleware functions
Returns: Store object with state (signals) and action methods
Middleware API
type Middleware<S> = (
next: () => void,
actionName: string,
args: any[],
context: {
state: SignalState<S>
getSnapshot: () => S
}
) => voidCustom middleware example:
const myMiddleware = (next, actionName, args, context) => {
console.log(`Before ${actionName}`)
next() // Call the action
console.log(`After ${actionName}`)
}Comparison
| Feature | Boxstore | Redux | Zustand | Jotai | Pinia | |---------|----------|-------|---------|-------|-------| | Bundle Size | < 3KB | ~8KB | ~1KB | ~3KB | Framework | | Reactivity | Signals | Subscribe | Subscribe | Atoms | Vue refs | | Actions | ✅ | ✅ | ✅ | ❌ | ✅ | | Middleware | ✅ | ✅ | ✅ | ❌ | ✅ | | TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ | | Framework | Agnostic | Agnostic | Agnostic | React | Vue | | DevTools | ✅ | ✅ | ✅ | ✅ | ✅ |
License
MIT © Varbyte
