@tanstack/store vs Zustand vs Nanostores State 2026
@tanstack/store vs Zustand vs Nanostores: Framework-Agnostic State Management in 2026
TL;DR
Most state management libraries are framework-coupled: Zustand is React-first, Pinia is Vue-only, Svelte stores are Svelte-only. But as multi-framework architectures become more common — islands architectures, shared business logic across React and React Native, micro-frontends mixing frameworks — framework-agnostic state stores matter. In 2026, three libraries lead this niche: @tanstack/store (the newest, from the TanStack team — fine-grained reactivity with React/Solid/Vue/Svelte adapters), Zustand (React-first but works outside React via getState()/subscribe()), and Nanostores (the smallest — <1KB, designed specifically for multi-framework use). For pure React apps, Zustand remains the default. For cross-framework needs, @tanstack/store or Nanostores are the right tools.
Key Takeaways
- @tanstack/store is a new framework-agnostic reactive store from TanStack — uses fine-grained computed selectors, adapters for React/Vue/Solid/Svelte, ~3KB
- Zustand is React-first but usable outside React via
useStore.getState()andsubscribe()— the store itself is framework-free - Nanostores is the most explicit multi-framework library — designed from day one for React, Vue, Svelte, Solid, and Preact with first-class adapters and atom composition
- Bundle sizes: Nanostores (<1KB per atom) < @tanstack/store (~3KB) < Zustand (~3KB with React adapter)
- TanStack Store introduces computed stores (derived state) as first-class primitives — no need for selectors or middleware
- When framework matters: Zustand has 20M+ weekly downloads and 12K+ GitHub stars; @tanstack/store is newer (~200K downloads) but backed by the TanStack team's track record
Why Framework-Agnostic State Management?
Most apps use one framework — and for those apps, framework-coupled state libraries (Zustand for React, Pinia for Vue) are the better choice. The multi-framework use case arises in:
- Islands architecture: Different islands use different frameworks (React island + Vue island on the same page sharing cart state)
- React + React Native: Sharing business logic between web (React) and mobile (React Native) apps
- Micro-frontends: Teams using different frameworks that need to share global state
- Framework-agnostic libraries: Building a UI component library that works across React, Vue, and Svelte
- Server → Client state hydration: State defined on the server needs to work before any framework hydrates
@tanstack/store: The New Entrant
TanStack Store was released in 2024 as a low-level reactive primitive that TanStack's other libraries (TanStack Router, TanStack Query internals) build on. It's now available as a standalone library.
Core API
import { Store } from '@tanstack/store'
// Framework-agnostic store
const cartStore = new Store({
items: [] as CartItem[],
total: 0,
isCheckingOut: false,
})
// Read state
const currentItems = cartStore.state.items
// Update state
cartStore.setState((state) => ({
...state,
items: [...state.items, newItem],
total: calculateTotal([...state.items, newItem]),
}))
// Subscribe to changes
const unsubscribe = cartStore.subscribe(() => {
console.log('Cart changed:', cartStore.state)
})
// Cleanup
unsubscribe()
Derived Stores (Computed State)
TanStack Store's killer feature is derived stores — computed values that automatically update when dependencies change:
import { Store, Derived } from '@tanstack/store'
const cartStore = new Store({
items: [] as CartItem[],
promoCode: null as string | null,
})
// Derived store — automatically recomputes when cartStore changes
const cartSummary = new Derived({
deps: [cartStore],
fn: ({ deps: [cart] }) => ({
subtotal: cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
itemCount: cart.items.reduce((sum, item) => sum + item.quantity, 0),
hasItems: cart.items.length > 0,
}),
})
cartSummary.mount() // Start computing
// Read the derived state
const { subtotal, itemCount } = cartSummary.state
// Another derived store — dependency chained
const finalTotal = new Derived({
deps: [cartStore, cartSummary],
fn: ({ deps: [cart, summary] }) => ({
total: cart.promoCode === 'SAVE10'
? summary.subtotal * 0.9
: summary.subtotal,
discount: cart.promoCode ? summary.subtotal * 0.1 : 0,
}),
})
React Adapter
import { useStore } from '@tanstack/react-store'
function CartSummary() {
// Subscribe to specific derived state — only re-renders when selected slice changes
const { subtotal, itemCount } = useStore(cartSummary, (s) => ({
subtotal: s.subtotal,
itemCount: s.itemCount,
}))
return (
<div>
<span>{itemCount} items</span>
<span>${subtotal.toFixed(2)}</span>
</div>
)
}
function CheckoutButton() {
// Subscribe to cartStore directly
const hasItems = useStore(cartStore, (state) => state.items.length > 0)
return (
<button
disabled={!hasItems}
onClick={() => cartStore.setState(s => ({ ...s, isCheckingOut: true }))}
>
Checkout
</button>
)
}
Vue and Svelte Adapters
// Vue adapter
import { useStore } from '@tanstack/vue-store'
const CartSummary = defineComponent({
setup() {
const subtotal = useStore(cartSummary, (s) => s.subtotal)
const itemCount = useStore(cartSummary, (s) => s.itemCount)
return { subtotal, itemCount }
},
})
// Svelte adapter (Svelte 5 with runes)
import { useStore } from '@tanstack/svelte-store'
const subtotal = useStore(cartSummary, (s) => s.subtotal)
// $subtotal.current in template
Zustand: React-First but Framework-Portable
Zustand (5M+ weekly downloads) is React-first but its store internals are framework-agnostic. The create() function returns a hook for React and a vanilla store:
import { create } from 'zustand'
import { createStore } from 'zustand/vanilla'
// Vanilla store — zero framework dependency
const vanillaCartStore = createStore<CartState>((set, get) => ({
items: [],
total: 0,
addItem: (item: CartItem) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id: string) => {
const items = get().items.filter(i => i.id !== id)
set({ items, total: items.reduce((sum, i) => sum + i.price, 0) })
},
clear: () => set({ items: [], total: 0 }),
}))
// Use outside any framework
vanillaCartStore.getState().addItem({ id: '1', name: 'Widget', price: 10 })
vanillaCartStore.subscribe((state) => console.log('Cart:', state))
// Wrap for React
import { useStore } from 'zustand'
const useCartStore = (selector) => useStore(vanillaCartStore, selector)
function CartCount() {
const count = useCartStore((state) => state.items.length)
return <span>{count} items</span>
}
Zustand's vanilla package is fully framework-agnostic. The create() shortcut is just a convenience wrapper that creates both the vanilla store and the React hook simultaneously.
Nanostores: Built for Multi-Framework From Day One
Nanostores is the most explicit multi-framework state library. It uses an atom model (like Jotai, but even smaller) with first-class framework adapters:
npm install nanostores
npm install @nanostores/react # React adapter
npm install @nanostores/vue # Vue adapter
npm install @nanostores/persistent # localStorage persistence
Atom-Based State
import { atom, map, computed } from 'nanostores'
// Primitive atoms
const isLoggedIn = atom(false)
const currentUserId = atom<string | null>(null)
// Map atom (object state, granular updates)
const userPreferences = map({
theme: 'light' as 'light' | 'dark',
language: 'en',
notificationsEnabled: true,
})
// Computed atoms (derived from other atoms)
const greeting = computed(
[isLoggedIn, currentUserId],
(loggedIn, userId) =>
loggedIn ? `Welcome back, ${userId}` : 'Please sign in'
)
Framework Adapters
// React
import { useStore } from '@nanostores/react'
function Header() {
const loggedIn = useStore(isLoggedIn)
const greeting = useStore(greeting)
const preferences = useStore(userPreferences)
return (
<header data-theme={preferences.theme}>
{loggedIn ? greeting : <LoginButton />}
</header>
)
}
// Vue 3
import { useStore } from '@nanostores/vue'
const App = defineComponent({
setup() {
const loggedIn = useStore(isLoggedIn)
const theme = computed(() => userPreferences.get().theme)
return { loggedIn, theme }
},
})
// Svelte — uses native stores protocol (no adapter needed)
// Nanostores atoms ARE Svelte stores
<script>
import { isLoggedIn } from './stores'
</script>
{#if $isLoggedIn}
<Dashboard />
{/if}
Sharing State Between React and Vue
The multi-framework power comes from sharing atoms across framework boundaries:
// stores/cart.ts — framework-agnostic
import { atom, map, computed } from 'nanostores'
export const cartItems = atom<CartItem[]>([])
export const cartTotal = computed(
cartItems,
(items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
export function addToCart(item: CartItem) {
cartItems.set([...cartItems.get(), item])
}
// react-island/Cart.tsx — React cart component
import { useStore } from '@nanostores/react'
import { cartItems, cartTotal, addToCart } from '../stores/cart'
// vue-island/Cart.vue — Vue cart component
import { useStore } from '@nanostores/vue'
import { cartItems, cartTotal, addToCart } from '../stores/cart'
// Both components read from the SAME cartItems atom
// Updating from one framework updates the other
Persistence and Middleware
Each library handles persistence and middleware differently:
Zustand Middleware
Zustand has a rich middleware ecosystem:
import { create } from 'zustand'
import { persist, devtools, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
const useCartStore = create<CartState>()(
devtools( // Redux DevTools integration
persist( // localStorage persistence
subscribeWithSelector( // Subscribe to specific slices
immer((set) => ({ // Immer for immutable updates
items: [],
addItem: (item) => set((state) => {
// Immer lets you "mutate" — it produces immutable updates
state.items.push(item)
}),
}))
),
{
name: 'cart-storage',
partialize: (state) => ({ items: state.items }), // Only persist items
}
)
)
)
// Subscribe to specific slice — callback only fires when items.length changes
useCartStore.subscribe(
(state) => state.items.length,
(length) => console.log('Cart item count changed:', length)
)
Nanostores Persistence
Nanostores uses the separate @nanostores/persistent package:
import { persistentAtom, persistentMap } from '@nanostores/persistent'
// Persisted atom — syncs with localStorage
const theme = persistentAtom<'light' | 'dark'>('theme', 'light')
// Persisted map — saves entire object to localStorage
const userPrefs = persistentMap('user-prefs:', {
language: 'en',
currency: 'USD',
fontSize: 16,
})
// Cross-tab sync — updates in one tab reflect in other tabs
// This works via storage events, no config needed
theme.set('dark') // All open tabs switch to dark mode
@tanstack/store Middleware
TanStack Store supports middleware via the onUpdate option:
import { Store } from '@tanstack/store'
const cartStore = new Store({
items: [] as CartItem[],
}, {
onUpdate: () => {
// Runs after every state update
localStorage.setItem('cart', JSON.stringify(cartStore.state.items))
},
})
// Load persisted state on init
const saved = localStorage.getItem('cart')
if (saved) {
cartStore.setState((s) => ({ ...s, items: JSON.parse(saved) }))
}
TanStack Store's middleware story is less mature than Zustand's — this is an area actively being developed as the library grows.
Devtools Support
| Library | Redux DevTools | Dedicated DevTools |
|---|---|---|
| Zustand | ✅ via middleware | Browser extension |
| @tanstack/store | ❌ | TanStack Query DevTools (separate) |
| Nanostores | ❌ | @nanostores/logger for console logging |
Zustand has the most mature devtools story thanks to its Redux DevTools middleware. If time-travel debugging is important to your team's workflow, Zustand is the only option with mature support.
Bundle Size Comparison
| Library | Core size | With React adapter |
|---|---|---|
| Nanostores | 814B | +350B (@nanostores/react) |
| @tanstack/store | ~3.1KB | +1.2KB (@tanstack/react-store) |
| Zustand (vanilla) | ~1.0KB | +900B (zustand hooks) |
| Jotai | ~4.5KB | built-in |
| Recoil | ~22KB | built-in |
Nanostores is unambiguously the smallest option. For SSG/island architectures where JavaScript size is a primary concern, the sub-1KB bundle is meaningful.
When to Use Each
Use @tanstack/store when:
- You're already using TanStack Query/Router and want consistent primitives
- You need first-class derived/computed state (Derived stores)
- You want explicit framework adapter imports (clear separation of concerns)
- Your team is multi-framework and TanStack's ecosystem is your foundation
Use Zustand (vanilla) when:
- Your app is primarily React but shares some state with non-React code
- You want Zustand's large ecosystem (middlewares, devtools, persist)
- You want the simplest possible API with lowest learning curve
- You're OK with React being a first-class citizen and other frameworks as second-class
Use Nanostores when:
- Bundle size is critical (islands architecture, SSG sites, embedded widgets)
- You need true multi-framework first-class support (React + Vue on the same page)
- You prefer atom-based composition (Jotai-style) over single-store models
- Svelte is one of your target frameworks (Nanostores works as native Svelte stores)
Methodology
- Download data from npmjs.com API, March 2026 weekly averages
- Bundle sizes from bundlephobia.com (minzipped), March 2026
- Versions: @tanstack/store 0.7.x, Zustand 5.x, Nanostores 0.11.x
- Sources: TanStack Store documentation, Zustand documentation, Nanostores GitHub
Compare @tanstack/store, Zustand, and Nanostores on PkgPulse — download trends, bundle size analysis, and health scores.
Related: Zustand vs Jotai vs Nanostores Micro State Management 2026 · MobX vs Valtio vs Legend-State Observable State Management 2026 · Preact Signals vs React useState vs Jotai Fine-Grained Reactivity 2026