@varbyte/boxstore-preact
v0.1.0
Published
Preact hooks for @varbyte/boxstore
Maintainers
Readme
@varbyte/boxstore-preact
Preact adapter for boxstore - providing hooks to integrate boxstore's signal-based state management with Preact components.
Features
- Granular subscriptions - Components only re-render when selected state changes
- Type-safe - Full TypeScript support with type inference
- Preact 10+ compatible - Uses
useEffect+useStatepattern - SSR-ready - Works with server-side rendering
- Tiny - Minimal bundle size impact (smaller than React adapter)
Installation
npm install @varbyte/boxstore-preact @varbyte/boxstore @varbyte/signals-core preactRequirements
- Preact >= 10.0.0
- @varbyte/boxstore >= 0.1.0
- @varbyte/signals-core >= 1.0.0
API Reference
useSelector(store, selector)
Subscribe to a selected slice of store state. Re-renders only when the selected value changes.
Parameters:
store: Store<S, A>- A boxstore store instanceselector: (store: Store<S, A>) => R- Function that reads signals and returns derived value
Returns: R - The current selected value
Example:
import { useSelector } from '@varbyte/boxstore-preact'
import { myStore } from './store'
function Counter() {
const count = useSelector(myStore, (s) => s.state.count())
return (
<div>
<p>Count: {count}</p>
<button onClick={() => myStore.increment()}>Increment</button>
</div>
)
}useStore(store)
Subscribe to entire store state. Re-renders when any state property changes.
Parameters:
store: Store<S, A>- A boxstore store instance
Returns: S - Plain object snapshot of all state values (not signals)
Example:
import { useStore } from '@varbyte/boxstore-preact'
import { myStore } from './store'
function Dashboard() {
const state = useStore(myStore)
return (
<div>
<p>Count: {state.count}</p>
<p>Name: {state.name}</p>
</div>
)
}Usage Examples
Basic Counter
import { createStore } from '@varbyte/boxstore'
import { useSelector } from '@varbyte/boxstore-preact'
// Create store
const counterStore = createStore({
state: { count: 0 },
actions: {
increment() {
this.state.count.update(n => n + 1)
},
decrement() {
this.state.count.update(n => n - 1)
}
}
})
// Use in component
function Counter() {
const count = useSelector(counterStore, (s) => s.state.count())
return (
<div>
<p>Count: {count}</p>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.decrement()}>-</button>
</div>
)
}Todo List with Granular Subscriptions
import { createStore } from '@varbyte/boxstore'
import { useSelector } from '@varbyte/boxstore-preact'
const todoStore = createStore({
state: {
todos: [] as Array<{ id: number; text: string; done: boolean }>,
filter: 'all' as 'all' | 'active' | 'completed'
},
actions: {
addTodo(text: string) {
const newTodo = { id: Date.now(), text, done: false }
this.state.todos.update(todos => [...todos, newTodo])
},
toggleTodo(id: number) {
this.state.todos.update(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
)
},
setFilter(filter: 'all' | 'active' | 'completed') {
this.state.filter.set(filter)
}
}
})
// Component only re-renders when filtered todos change
function TodoList() {
const todos = useSelector(todoStore, (s) => {
const allTodos = s.state.todos()
const filter = s.state.filter()
if (filter === 'all') return allTodos
if (filter === 'active') return allTodos.filter(t => !t.done)
return allTodos.filter(t => t.done)
})
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}
// Filter component only re-renders when filter changes (not when todos change)
function FilterButtons() {
const filter = useSelector(todoStore, (s) => s.state.filter())
return (
<div>
<button onClick={() => todoStore.setFilter('all')} disabled={filter === 'all'}>
All
</button>
<button onClick={() => todoStore.setFilter('active')} disabled={filter === 'active'}>
Active
</button>
<button onClick={() => todoStore.setFilter('completed')} disabled={filter === 'completed'}>
Completed
</button>
</div>
)
}Derived State
import { useSelector } from '@varbyte/boxstore-preact'
function Stats() {
const stats = useSelector(todoStore, (s) => {
const todos = s.state.todos()
return {
total: todos.length,
completed: todos.filter(t => t.done).length,
active: todos.filter(t => !t.done).length
}
})
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Active: {stats.active}</p>
</div>
)
}Best Practices
Selector Stability
Inline arrow functions are fine! The hook uses useRef internally to avoid unnecessary re-subscriptions:
// ✅ This is fine - no need to memoize
function MyComponent() {
const count = useSelector(store, (s) => s.state.count())
return <div>{count}</div>
}For complex selectors that create new objects, consider using useCallback from preact/hooks to avoid creating new selector references on every render:
import { useCallback } from 'preact/hooks'
// ✅ Good - memoized selector for derived object
function MyComponent() {
const selector = useCallback(
(s) => ({ count: s.state.count(), name: s.state.name() }),
[]
)
const data = useSelector(store, selector)
return <div>{data.count} - {data.name}</div>
}Object Identity in Selectors
Be careful when returning new objects from selectors. Since the hook checks value equality, returning a new object reference each time will cause re-renders:
// ⚠️ Creates new object on each call - may cause extra renders
const data = useSelector(store, (s) => ({
count: s.state.count(),
name: s.state.name()
}))For primitive values, this is not an issue:
// ✅ Primitives are compared by value
const count = useSelector(store, (s) => s.state.count())Preact Signals Compatibility
This adapter is designed for @varbyte/boxstore which uses @varbyte/signals-core. If you're using Preact's built-in @preact/signals, that's a different signals library and won't work with boxstore.
// ✅ Correct - boxstore with signals-core
import { createStore } from '@varbyte/boxstore'
import { useSelector } from '@varbyte/boxstore-preact'
// ❌ Wrong - don't mix with @preact/signals
import { signal } from '@preact/signals'TypeScript
Type inference works automatically:
const store = createStore({
state: {
count: 0,
user: null as { id: number; name: string } | null
},
actions: {}
})
// Type inferred: number
const count = useSelector(store, (s) => s.state.count())
// Type inferred: { id: number; name: string } | null
const user = useSelector(store, (s) => s.state.user())
// Type inferred: { count: number; user: { id: number; name: string } | null }
const state = useStore(store)Differences from React Adapter
The Preact adapter uses useEffect + useState instead of useSyncExternalStore (which doesn't exist in Preact):
- No concurrent mode - Preact doesn't have concurrent rendering, so the simpler pattern works perfectly
- Smaller bundle - No need for
useSyncExternalStorepolyfill - Same API - Drop-in replacement for React adapter with identical behavior
Links
License
MIT
