<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/tanstack-store-vs-zustand-vs-nanostores-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/tanstack-store-vs-zustand-vs-nanostores-2026/raw.md -->
<!-- Source path: content/guides/tanstack-store-vs-zustand-vs-nanostores-2026.mdx -->

---
og_image: "/images/guides/tanstack-store-vs-zustand-vs-nanostores-2026.webp"
title: "@tanstack/store vs Zustand vs Nanostores State 2026"
description: "@tanstack/store vs Zustand vs Nanostores for framework-agnostic state management in 2026: API comparison, bundle size, React/Vue/Svelte adapters, and when to."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["state-management", "tanstack", "zustand", "javascript", "react"]
---

# @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](https://tanstack.com/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

```typescript
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:

```typescript
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

```typescript
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

```typescript
// 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](https://zustand.docs.pmnd.rs/) (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:

```typescript
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](https://github.com/nanostores/nanostores) is the most explicit multi-framework state library. It uses an **atom** model (like Jotai, but even smaller) with first-class framework adapters:

```bash
npm install nanostores
npm install @nanostores/react    # React adapter
npm install @nanostores/vue      # Vue adapter
npm install @nanostores/persistent  # localStorage persistence
```

### Atom-Based State

```typescript
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

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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

---

## 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](/compare) — download trends, bundle size analysis, and health scores.*

*Related: [Zustand vs Jotai vs Nanostores Micro State Management 2026](/guides/zustand-vs-jotai-vs-nanostores-micro-state-management-2026) · [MobX vs Valtio vs Legend-State Observable State Management 2026](/guides/mobx-vs-valtio-vs-legend-state-observable-state-2026) · [Preact Signals vs React useState vs Jotai Fine-Grained Reactivity 2026](/guides/preact-signals-vs-react-usestate-vs-jotai-fine-grained-2026)*
