Skip to main content

@tanstack/store vs Zustand vs Nanostores State 2026

·PkgPulse Team

@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() and subscribe() — 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:

  1. Islands architecture: Different islands use different frameworks (React island + Vue island on the same page sharing cart state)
  2. React + React Native: Sharing business logic between web (React) and mobile (React Native) apps
  3. Micro-frontends: Teams using different frameworks that need to share global state
  4. Framework-agnostic libraries: Building a UI component library that works across React, Vue, and Svelte
  5. 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

LibraryRedux DevToolsDedicated DevTools
Zustand✅ via middlewareBrowser extension
@tanstack/storeTanStack 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

LibraryCore sizeWith React adapter
Nanostores814B+350B (@nanostores/react)
@tanstack/store~3.1KB+1.2KB (@tanstack/react-store)
Zustand (vanilla)~1.0KB+900B (zustand hooks)
Jotai~4.5KBbuilt-in
Recoil~22KBbuilt-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

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.