@varbyte/boxstore-astro
v0.1.0
Published
Astro integration for @varbyte/boxstore with React and Preact support
Maintainers
Readme
@varbyte/boxstore-astro
Astro integration for boxstore - enabling signal-based state management across React and Preact islands in Astro applications.
Features
- Cross-island state sharing - Module-level singleton stores work across all islands in a page
- Framework flexibility - Use React or Preact hooks via subpath exports
- Optional Context API -
StoreProviderfor intra-island React Context when needed - Type-safe - Full TypeScript support with type inference
- SSR-ready - Works with Astro's server-side rendering
- Tiny - Minimal bundle size impact
Installation
npm install @varbyte/boxstore-astro @varbyte/boxstore @varbyte/signals-core astroFor React islands:
npm install react react-dom @varbyte/boxstore-reactFor Preact islands:
npm install preact @varbyte/boxstore-preactRequirements
- Astro >= 4.0.0
- @varbyte/boxstore >= 0.1.0
- @varbyte/signals-core >= 1.0.0
- React >= 18.0.0 (for React islands)
- Preact >= 10.0.0 (for Preact islands)
Quick Start
1. Create a store module
// src/stores/counter.ts
import { createStore } from '@varbyte/boxstore'
export const counterStore = createStore({
state: {
count: 0
},
actions: {
increment() {
this.state.count.update(n => n + 1)
},
decrement() {
this.state.count.update(n => n - 1)
}
}
})2. Use in React islands
// src/components/Counter.tsx
import { useSelector } from '@varbyte/boxstore-astro/react'
import { counterStore } from '../stores/counter'
export 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>
)
}3. Use in Preact islands
// src/components/Display.tsx
import { useSelector } from '@varbyte/boxstore-astro/preact'
import { counterStore } from '../stores/counter'
export function Display() {
const count = useSelector(counterStore, s => s.state.count())
return <div>Current count: {count}</div>
}4. Add islands to Astro page
---
// src/pages/index.astro
import { Counter } from '../components/Counter'
import { Display } from '../components/Display'
---
<html>
<body>
<h1>Boxstore Astro Demo</h1>
<!-- React island -->
<Counter client:load />
<!-- Preact island -->
<Display client:load />
<!-- Both islands share the same store state! -->
</body>
</html>Architecture: Cross-Island State Sharing
Astro islands are independently hydrated and do NOT share React/Preact context trees. To share state across islands, use module-level singleton stores:
// src/stores/app.ts
import { createStore } from '@varbyte/boxstore'
// This store instance is shared across all islands that import it
export const appStore = createStore({
state: { theme: 'light' },
actions: {}
})When multiple islands import appStore, they all get the same instance because ES modules are singletons within the same page bundle.
API Reference
Subpath Exports
@varbyte/boxstore-astro/react
Re-exports hooks from @varbyte/boxstore-react for use in React islands.
import { useSelector, useStore } from '@varbyte/boxstore-astro/react'Exports:
useSelector(store, selector)- Subscribe to selected state sliceuseStore(store)- Subscribe to entire store- Types:
Selector<S, A, R>,AnyStore
See @varbyte/boxstore-react documentation for detailed API.
@varbyte/boxstore-astro/preact
Re-exports hooks from @varbyte/boxstore-preact for use in Preact islands.
import { useSelector, useStore } from '@varbyte/boxstore-astro/preact'Exports:
useSelector(store, selector)- Subscribe to selected state sliceuseStore(store)- Subscribe to entire store- Types:
Selector<S, A, R>,AnyStore
See @varbyte/boxstore-preact documentation for detailed API.
Main Export: StoreProvider
Optional React Context provider for intra-island store injection. Use this when multiple components within a single island need access to a store via context.
Important: StoreProvider only works within a single island. It does NOT share state across multiple islands. For cross-island sharing, use module-level stores.
import { StoreProvider, useStoreContext } from '@varbyte/boxstore-astro'StoreProvider<S, A>
Provides a store via React Context to descendant components.
Props:
store: Store<S, A>- The boxstore instance to providechildren: React.ReactNode- Child components
Example:
// src/components/Island.tsx
import { StoreProvider } from '@varbyte/boxstore-astro'
import { useSelector } from '@varbyte/boxstore-astro/react'
import { createStore } from '@varbyte/boxstore'
const localStore = createStore({
state: { message: 'Hello' },
actions: {}
})
function ChildComponent() {
const store = useStoreContext()
const message = useSelector(store, s => s.state.message())
return <p>{message}</p>
}
export function Island() {
return (
<StoreProvider store={localStore}>
<ChildComponent />
</StoreProvider>
)
}useStoreContext<S, A>()
Retrieves the store from the nearest StoreProvider ancestor.
Returns: Store<S, A> - The store instance from context
Throws: Error if used outside of a StoreProvider
Example:
import { useStoreContext } from '@varbyte/boxstore-astro'
import { useSelector } from '@varbyte/boxstore-astro/react'
function Child() {
const store = useStoreContext()
const value = useSelector(store, s => s.state.value())
return <div>{value}</div>
}Usage Patterns
Pattern 1: Module-Level Singleton (Recommended)
Best for cross-island state sharing.
// src/stores/user.ts
import { createStore } from '@varbyte/boxstore'
export const userStore = createStore({
state: {
name: 'Guest',
isAuthenticated: false
},
actions: {
login(name: string) {
this.state.name.set(name)
this.state.isAuthenticated.set(true)
},
logout() {
this.state.name.set('Guest')
this.state.isAuthenticated.set(false)
}
}
})// Any island can import and use it
import { useSelector } from '@varbyte/boxstore-astro/react'
import { userStore } from '../stores/user'
export function UserBadge() {
const name = useSelector(userStore, s => s.state.name())
return <span>Welcome, {name}</span>
}Pattern 2: StoreProvider for Local State
Use when a store is local to a single island and you want context-based injection.
// src/components/TodoIsland.tsx
import { createStore } from '@varbyte/boxstore'
import { StoreProvider, useStoreContext } from '@varbyte/boxstore-astro'
import { useSelector } from '@varbyte/boxstore-astro/react'
const todoStore = createStore({
state: { todos: [] },
actions: {
addTodo(text: string) {
this.state.todos.update(todos => [...todos, { id: Date.now(), text }])
}
}
})
function TodoList() {
const store = useStoreContext()
const todos = useSelector(store, s => s.state.todos())
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)
}
function AddTodo() {
const store = useStoreContext()
return (
<button onClick={() => store.addTodo('New task')}>
Add Todo
</button>
)
}
export function TodoIsland() {
return (
<StoreProvider store={todoStore}>
<TodoList />
<AddTodo />
</StoreProvider>
)
}Pattern 3: Mixed React and Preact Islands
Share state between React and Preact islands using a module-level store.
// src/stores/theme.ts
import { createStore } from '@varbyte/boxstore'
export const themeStore = createStore({
state: {
mode: 'light' as 'light' | 'dark'
},
actions: {
toggle() {
this.state.mode.update(m => m === 'light' ? 'dark' : 'light')
}
}
})// src/components/ThemeToggle.tsx (React)
import { useSelector } from '@varbyte/boxstore-astro/react'
import { themeStore } from '../stores/theme'
export function ThemeToggle() {
const mode = useSelector(themeStore, s => s.state.mode())
return (
<button onClick={() => themeStore.toggle()}>
Current: {mode}
</button>
)
}// src/components/ThemeDisplay.tsx (Preact)
import { useSelector } from '@varbyte/boxstore-astro/preact'
import { themeStore } from '../stores/theme'
export function ThemeDisplay() {
const mode = useSelector(themeStore, s => s.state.mode())
return <div>Theme is {mode}</div>
}Both islands will stay in sync because they share the same themeStore instance.
Best Practices
1. Prefer Module-Level Stores
For most use cases, create stores at module level rather than using StoreProvider:
// ✅ Good - module-level singleton
export const store = createStore({ ... })
// ❌ Avoid - creating new store in component (unless truly local)
function Component() {
const store = createStore({ ... }) // New instance every render!
// ...
}2. Use Granular Selectors
Select only the state you need to minimize re-renders:
// ✅ Good - only re-renders when count changes
const count = useSelector(store, s => s.state.count())
// ❌ Avoid - re-renders when any state changes
const state = useStore(store)
const count = state.count3. Choose the Right Subpath Export
Import from the correct subpath for your island's framework:
// For React islands
import { useSelector } from '@varbyte/boxstore-astro/react'
// For Preact islands
import { useSelector } from '@varbyte/boxstore-astro/preact'4. Type Your Stores
Define explicit types for better TypeScript inference:
interface UserState {
name: string
email: string
}
interface UserActions {
updateEmail(email: string): void
}
export const userStore = createStore<UserState, UserActions>({
state: {
name: 'Guest',
email: ''
},
actions: {
updateEmail(email) {
this.state.email.set(email)
}
}
})TypeScript Support
All exports are fully typed. TypeScript will infer return types from your selectors:
const store = createStore({
state: {
count: 0,
user: { name: 'Alice', age: 30 }
},
actions: {}
})
// TypeScript infers: const count: number
const count = useSelector(store, s => s.state.count())
// TypeScript infers: const name: string
const name = useSelector(store, s => s.state.user().name)Troubleshooting
Store state not syncing between islands
Problem: Changes in one island don't reflect in another.
Solution: Ensure both islands import the store from the same module path. ES module identity must match:
// ✅ Both import from same path
import { store } from '../stores/app'
// ❌ Different relative paths might resolve to different modules
import { store } from '../../stores/app'"useStoreContext must be used within StoreProvider"
Problem: Calling useStoreContext() outside of a StoreProvider.
Solution: Either wrap your component in StoreProvider or import the store directly:
// Option 1: Use StoreProvider
<StoreProvider store={myStore}>
<Component />
</StoreProvider>
// Option 2: Import store directly (preferred for most cases)
import { myStore } from '../stores/my-store'
const value = useSelector(myStore, s => s.state.value())Module not found errors
Problem: Cannot find module '@varbyte/boxstore-astro/react'
Solution: Ensure you've installed the corresponding adapter package:
npm install @varbyte/boxstore-react # for /react subpath
npm install @varbyte/boxstore-preact # for /preact subpathExamples
See the examples directory for complete working demos including:
- Counter with mixed React/Preact islands
- Theme switcher across islands
- Todo list with local state
- User authentication flow
License
MIT
Related Packages
- @varbyte/boxstore - Core state management library
- @varbyte/boxstore-react - React hooks adapter
- @varbyte/boxstore-preact - Preact hooks adapter
- @varbyte/signals-core - Underlying signals library
