Auto-tracking
Read state → Storion tracks it. No manual dependency arrays or selectors to maintain.
Write state naturally. Let Storion handle the rest.
Here's a complete, working counter:
import { store, StoreProvider, useStore } from 'storion/react'
// STEP 1: Define your store ─────────────────────────────────────────────────
const counterStore = store({
name: 'counter', // Name appears in DevTools for easy debugging
state: { count: 0 }, // Initial state — becomes reactive automatically
// Setup runs once when store is first used
setup({ state }) {
return {
// Actions: just mutate state directly!
// Storion wraps state in a Proxy, so mutations trigger updates
increment: () => { state.count++ },
decrement: () => { state.count-- },
reset: () => { state.count = 0 },
}
},
})
// STEP 2: Use in React ──────────────────────────────────────────────────────
function Counter() {
// The selector function receives a context with `get` to access stores
const { count, increment, decrement } = useStore(({ get }) => {
// get() returns [state, actions] tuple
const [state, actions] = get(counterStore)
// Return only what this component needs
// Storion tracks that we read `state.count` — so this component
// ONLY re-renders when `count` changes, not other state
return {
count: state.count,
increment: actions.increment,
decrement: actions.decrement,
}
})
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
// STEP 3: Wrap with Provider ────────────────────────────────────────────────
function App() {
return (
<StoreProvider>
<Counter />
</StoreProvider>
)
}create() insteadimport { create } from 'storion/react'
const [counter, useCounter] = create({
state: { count: 0 },
setup({ state }) {
return {
increment: () => { state.count++ },
decrement: () => { state.count-- },
}
},
})
// No Provider needed!
function Counter() {
const { count, increment, decrement } = useCounter((state, actions) => ({
count: state.count,
increment: actions.increment,
decrement: actions.decrement,
}))
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}| What You Write | What Storion Does |
|---|---|
state.count++ | Detects mutation → notifies subscribers |
state.count in selector | Tracks this property → re-renders when it changes |
StoreProvider | Manages store instances and enables sharing across components |
Copy, paste, run
The example above works as-is. Just npm install storion and try it.
// ❌ Zustand: One store per hook, manual selectors, memoize actions
function Component({ step }) {
// Returning object? Need shallow compare or it re-renders every time!
const { count, name } = useCounterStore(
(state) => ({ count: state.count, name: state.name }),
shallow // forget this? re-renders on EVERY state change!
)
// Action uses props → need useCallback with deps
const increment = useCallback(() => {
useCounterStore.getState().incrementBy(step)
}, [step]) // forget this dep? stale closure bug!
return <ChildComponent onIncrement={increment} />
}
// ✅ Storion: Tracks state access, not selector result
function Component({ step }) {
const { count, name, increment } = useStore(({ get }) => {
const [counter, counterActions] = get(counterStore)
const [user] = get(userStore)
return {
count: counter.count, // ← Storion tracks this access
name: user.name, // ← and this access
increment: () => counterActions.incrementBy(step), // ← auto-stable!
}
})
// No shallow compare — we track state.count and state.name, not result object
// increment is auto-stabilized — same reference, always fresh `step` value
return <ChildComponent onIncrement={increment} />
}| Feature | Redux | Zustand | Jotai | Storion |
|---|---|---|---|---|
| Auto-tracking | ❌ Manual selectors | ❌ Manual selectors | ✅ | ✅ |
| Multiple stores | ✅ Single store | ⚠️ One hook each | ✅ Atoms | ✅ One hook |
| Cross-store deps | ⚠️ Verbose | ❌ Manual | ❌ | ✅ Auto-resolved |
| Stable actions | ⚠️ useCallback | ⚠️ Extra selectors | ⚠️ | ✅ Automatic |
| Object selectors | ⚠️ Reselect | ⚠️ Need shallow | ✅ | ✅ No compareFn |
| TypeScript | ⚠️ Verbose | ✅ Good | ✅ Good | ✅ Excellent |
| Dependency Injection | ❌ | ❌ | ❌ | ✅ Built-in |
| Async State | ❌ External lib | ⚠️ Basic | ⚠️ Basic | ✅ First-class |
| Middleware | ✅ Complex setup | ⚠️ Per-store only | ❌ | ✅ Global + per-store |
| DevTools | ✅ | ✅ | ⚠️ | ✅ |
| Bundle Size | ~2KB | ~1KB | ~2KB | ~4KB |
| Learning Curve | Steep | Easy | Medium | Easy |
Storion grows with your app. Here's a taste of what's possible:
import { async } from 'storion/async'
const userStore = store({
name: 'user',
state: {
// async.fresh() = undefined while loading, data after success
user: async.fresh<User>(),
},
setup({ focus }) {
// Create an async action bound to state.user
const userQuery = async(
focus('user'), // Links this action to state.user
async (ctx, id: string) => {
// ctx.signal auto-cancels on unmount or new request
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal })
return res.json()
}
)
return { fetchUser: userQuery.dispatch }
},
})const cartStore = store({
name: 'cart',
state: { items: [] },
setup({ state, get }) {
// Access other stores in setup
const [userState] = get(userStore)
return {
checkout: async () => {
// Use user state in cart actions
await api.checkout(userState.user.data?.id, state.items)
},
}
},
})import { persist } from 'storion/persist'
const settingsStore = store({
name: 'settings',
state: { theme: 'dark', fontSize: 14 },
meta: persist(), // ← Auto-saves to localStorage
})Ready to learn more?
These features are covered in the Getting Started guide.
See Storion in action:
Become a sponsor to support Storion development!