Skip to main content

Guide

@tanstack/store vs Zustand vs Nanostores State 2026

@tanstack/store vs Zustand vs Nanostores for framework-agnostic state management in 2026: API comparison, bundle size, React/Vue/Svelte adapters, and when to.

·PkgPulse Team·
0

@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

Migration Paths and Adoption Strategy

Adopting a new state management library in an existing application is rarely an all-or-nothing decision. Each of the three libraries is designed to coexist with other state solutions, enabling incremental adoption. Nanostores atoms can share state with a React Context provider — the atom holds the authoritative value, and the Context distributes it to components that haven't been migrated to useStore yet. This lets you migrate components one at a time without a big-bang rewrite.

Zustand's vanilla store API makes it particularly easy to integrate with existing non-React state (Redux, MobX, custom event emitters). The subscribe() method fires a callback on every state change, which can trigger updates in the existing state system. Conversely, the existing system can call vanillaStore.setState() to push state into Zustand. This bidirectional bridge enables a phased migration where new features use Zustand while existing features continue using the legacy state solution.

Server-Side Rendering and Hydration Safety

Framework-agnostic state management introduces unique challenges in server-side rendering scenarios. When a server renders HTML and the client hydrates it, shared state that was correct on the server must match what the client produces during hydration — any mismatch causes a hydration error. Nanostores handles this cleanly because atoms have no implicit global singleton: you create a new store instance per request on the server and pass the initial state to the client via serialization, where a fresh atom instance is initialized with the server's value. This avoids the "request bleed" problem where server-side state from one request leaks into another.

Zustand's vanilla store is a singleton by default when imported at the module level — the same cartStore instance serves all server-side requests in a Node.js server, which means concurrent requests can read each other's state. The correct SSR pattern with Zustand is to create the store inside each request handler using createStore() (not the create() shorthand), pass it down via React context, and serialize the final state to the HTML for client hydration. This pattern is well-documented in Zustand's docs but requires explicit architectural discipline that is easy to accidentally skip.

TanStack Store's Store class is instantiated explicitly (new Store(initialState)), which makes the correct SSR pattern more natural than Zustand's module-level singleton. Creating one store per request is the obvious usage pattern when you call new Store() explicitly. The client-side hydration then initializes a new store with the server-serialized state and mounts it before any components access it. TanStack Router's built-in integration with TanStack Store (when both are used together) handles this serialization-hydration cycle automatically, making the cross-environment state handoff transparent in the TanStack ecosystem.

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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.