首页 / TanStack 生态入门教程 / Store:响应式状态管理

TanStack 生态入门教程

Store:响应式状态管理

本教程共 38 篇 · 第 37 篇 · 更新于 2026-07-27 · 约 11 分钟阅读

TanStackTanStack 生态入门教程TanStack Store状态管理createStoreuseSelectoratom响应式

37. Store:响应式状态管理

本节目标:认识 TanStack Store 这个框架无关的响应式状态管理库,学会用 createStore 创建状态、用 useSelector 在 React 中订阅状态、用 atom 机制管理派生状态,理解它在 TanStack 生态中的角色。学完你能用 Store 做跨组件状态共享。

37.1 TanStack Store 是什么

前面讲的 Query、Router、Table、Form、Virtual 都是面向开发者的功能库。TanStack Store 不一样—它是个底层基础设施,很多 TanStack 库内部都在用它做状态管理。

打个比方:Query、Table 这些库是「前台」,Store 是「后台仓库」。前台用到的状态,很多存在仓库里。你平时不需要直接跟仓库打交道,但了解它的工作方式能帮你更好地理解整个生态。

Store 也能单独用来做应用的状态管理。它的定位是:

  • 框架无关:核心包不依赖任何框架,React/Vue/Solid 都有适配器。
  • 响应式:状态变化时自动通知订阅者。
  • 轻量:API 简单,学习成本低。
  • 类型安全:TypeScript 支持好,状态类型自动推导。
Note

本教程基于 TanStack Store v0.11。它是 v0 版本,API 还在演进但核心已经稳定。TanStack 生态内部已经在大量使用它。

37.2 安装

React 项目装 React 适配器:

npm install @tanstack/react-store

核心包 @tanstack/store 会自动作为依赖装上。如果不用框架(纯 JS/TS),直接装核心包:

npm install @tanstack/store

37.3 createStore:创建状态

createStore 是核心 API,创建一个状态容器。

37.3.1 基本用法

import { createStore } from '@tanstack/store'

// 创建一个 count 状态,初始值 0
const countStore = createStore(0)

// 读当前值
console.log(countStore.state) // 0

// 更新值
countStore.setState(() => 1)
console.log(countStore.state) // 1

// 直接设值(不用函数)
countStore.setState(5)
console.log(countStore.state) // 5

setState 有两种传参方式:

  • 函数形式setState((prev) => prev + 1) — 拿到旧值返回新值,适合基于旧值计算。
  • 直接值setState(5) — 直接设新值,适合明确知道新值的场景。
Tip

setState 类似 React 的 useState,但 Store 的状态可以在组件外部访问和修改。这是 Store 和 useState 的根本区别。

37.3.2 存储对象

状态不限于基本类型,对象、数组都行:

interface User {
  name: string
  age: number
}

const userStore = createStore<User>({
  name: '张三',
  age: 28,
})

// 更新对象状态(要返回完整的新对象)
userStore.setState((prev) => ({
  ...prev,
  age: 29, // 只改 age
}))
Warning

更新对象状态时必须返回完整的新对象,不能直接改属性:userStore.state.age = 29 不行。要返回 { ...prev, age: 29 }。这是响应式状态的基本原则—用新引用触发更新。

37.3.3 订阅变化

Store 的核心能力是订阅(Subscribe)—状态变化时通知你:

const countStore = createStore(0)

// 订阅状态变化
const { unsubscribe } = countStore.subscribe((state) => {
  console.log('count 变了:', state)
})

countStore.setState(() => 5) // 输出: "count 变了: 5"

// 不再需要时取消订阅
unsubscribe()

subscribe 返回一个 unsubscribe 函数,调它取消订阅。这在非框架代码里(比如工具函数、定时器)很有用。

37.4 派生状态(Derived Store)

派生状态是从其他状态计算出来的状态。比如 count 是基础状态,double = count * 2 是派生状态。count 变了,double 自动更新。

37.4.1 用函数创建派生 Store

createStore 传入函数时,创建的是派生 Store

const countStore = createStore(0)

// double 是派生 Store,依赖 countStore
const doubleStore = createStore(() => countStore.state * 2)

console.log(doubleStore.state) // 0

countStore.setState(() => 5)
console.log(doubleStore.state) // 10 (自动更新)

派生 Store 是只读的,不能对它调 setState。它的值完全由依赖的状态决定。

37.4.2 访问前一个值

派生函数能拿到前一次计算的值(prev):

// 累加器:每次 count 变化时把新值加到之前的总和上
const sumStore = createStore<number>((prev) => {
  return countStore.state + (prev ?? 0)
})

console.log(sumStore.state) // 0

countStore.setState(() => 1)
console.log(sumStore.state) // 1 (0 + 1)

countStore.setState(() => 2)
console.log(sumStore.state) // 3 (1 + 2)

prev 第一次是 undefined,用 ?? 0 给个默认值。

37.5 在 React 中使用:useSelector

Store 本身是框架无关的,在 React 里用 useSelector 订阅状态。

37.5.1 基本用法

import { createStore, useSelector } from '@tanstack/react-store'

// 在组件外创建 Store(全局状态)
const countStore = createStore(0)

function Counter() {
  // 订阅 countStore 的 state
  const count = useSelector(countStore, (state) => state)

  return (
    <div>
      <p>当前值: {count}</p>
      <button onClick={() => countStore.setState((prev) => prev + 1)}>
        +1
      </button>
    </div>
  )
}

useSelector 接收两个参数:Store 实例和选择器函数。选择器从状态中取出需要的部分。组件只订阅选择器返回的值,值变了才重新渲染。

37.5.2 选择器优化性能

useSelector 的精髓在于选择性订阅—只订阅状态的一部分,其他部分变了不触发重渲染:

const store = createStore({
  dogs: 0,
  cats: 0,
})

function DogCounter() {
  // 只订阅 dogs,cats 变了不会重渲染
  const dogs = useSelector(store, (state) => state.dogs)
  return <div>狗: {dogs}</div>
}

function CatCounter() {
  // 只订阅 cats,dogs 变了不会重渲染
  const cats = useSelector(store, (state) => state.cats)
  return <div>猫: {cats}</div>
}

function App() {
  return (
    <div>
      <DogCounter />
      <CatCounter />
      <button onClick={() => store.setState((s) => ({ ...s, dogs: s.dogs + 1 }))}>
        加狗
      </button>
      <button onClick={() => store.setState((s) => ({ ...s, cats: s.cats + 1 }))}>
        加猫
      </button>
    </div>
  )
}

点「加狗」时只有 DogCounter 重渲染,CatCounter 不受影响。这是 useSelector 的核心优势—精确控制重渲染范围。

Tip

不传选择器时 useSelector(store) 订阅整个状态。状态任何部分变了都会重渲染。能用选择器就用选择器,精确订阅。

37.5.3 shallow 比较

选择器返回新对象时,每次渲染都会创建新引用,导致不必要的重渲染。用 shallow 选项做浅比较:

import { useSelector } from '@tanstack/react-store'

const store = createStore({ a: 1, b: 2 })

// 不加 shallow:每次渲染返回新对象,即使值没变也会重渲染
const { a, b } = useSelector(store, (s) => ({ a: s.a, b: s.b }))

// 加 shallow:浅比较对象属性,属性没变就不重渲染
const { a, b } = useSelector(store, (s) => ({ a: s.a, b: s.b }), { shallow: true })

37.6 batch:批量更新

连续多次 setState 会触发多次通知。用 batch 把它们包起来,只在最后通知一次:

import { batch } from '@tanstack/store'

// 不用 batch:三次通知
countStore.setState(() => 1)
countStore.setState(() => 2)
countStore.setState(() => 3)

// 用 batch:一次通知(最终值 3)
batch(() => {
  countStore.setState(() => 1)
  countStore.setState(() => 2)
  countStore.setState(() => 3)
})

在 React 中,多次状态更新会导致多次重渲染。用 batch 包起来只渲染一次,性能更好。

37.7 atom 机制

Store v0.11 引入了 atom 机制,提供更细粒度的响应式状态管理。Atom 类似 Store,但更轻量、更灵活。

37.7.1 创建 atom

import { createAtom } from '@tanstack/store'

// 创建一个 atom,初始值 0
const countAtom = createAtom(0)

// 创建派生 atom
const doubleAtom = createAtom(() => countAtom.state * 2)

Atom 的 API 和 Store 几乎一样:createAtom 创建、.state 读值、subscribe 订阅。区别在于 atom 设计上更接近「信号(Signal)」模式。

37.7.2 在 React 中用 atom

import { createAtom, useAtom } from '@tanstack/react-store'

const countAtom = createAtom(0)

function Counter() {
  // useAtom 返回 [值, 设值函数],和 useState 类似
  const [count, setCount] = useAtom(countAtom)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
    </div>
  )
}

useAtom 返回一个元组 [value, setter],和 React 的 useState 体验一致。Atom 也能用 useSelector 订阅:

const count = useSelector(countAtom)
Note

Atom 和 Store 在 v0.11 里并存。Store 是更早的 API,atom 是新的方向。新项目可以用 atom,老代码用 Store 也没问题。两者的核心概念一致,API 形式不同。

37.8 Store 和 TanStack 其他库的关系

Store 不是孤立的,它是 TanStack 生态的底层状态引擎:

  • TanStack Query 内部用 Store 管理查询缓存状态。
  • TanStack Router 内部用 Store 管理路由状态。
  • TanStack Form 内部用 Store 管理表单状态。
  • TanStack Table 内部用 Store 管理表格状态。

你用 useQueryuseFormuseReactTable 时,底层都在用 Store 的响应式机制。这也是为什么这些库能做到精确的状态订阅和重渲染控制。

Tip

你通常不需要直接用 Store 来做应用状态管理—TanStack Query 管服务端状态,React 的 useState/useReducer 管本地状态,已经够用。Store 适合这些场景:跨组件共享的全局状态、需要框架无关的状态管理、或者你想深入理解 TanStack 库的内部机制。

37.9 完整示例:全局状态管理

用 Store 做一个简单的全局状态管理(类似 Redux 的轻量替代):

import { createStore, useSelector } from '@tanstack/react-store'

// === 定义全局状态 ===
interface AppState {
  user: { name: string; role: string } | null
  theme: 'light' | 'dark'
  notifications: string[]
}

const appStore = createStore<AppState>({
  user: null,
  theme: 'light',
  notifications: [],
})

// === 定义操作(actions) ===
export const actions = {
  login(name: string, role: string) {
    appStore.setState((s) => ({ ...s, user: { name, role } }))
  },
  logout() {
    appStore.setState((s) => ({ ...s, user: null }))
  },
  toggleTheme() {
    appStore.setState((s) => ({ ...s, theme: s.theme === 'light' ? 'dark' : 'light' }))
  },
  addNotification(message: string) {
    appStore.setState((s) => ({
      ...s,
      notifications: [...s.notifications, message],
    }))
  },
}

// === 在组件中使用 ===
function Header() {
  const user = useSelector(appStore, (s) => s.user)
  const theme = useSelector(appStore, (s) => s.theme)

  return (
    <header className={`p-4 ${theme === 'dark' ? 'bg-gray-800' : 'bg-white'}`}>
      {user ? (
        <span>欢迎, {user.name} ({user.role})</span>
      ) : (
        <span>未登录</span>
      )}
      <button onClick={actions.toggleTheme}>
        {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </header>
  )
}

function NotificationList() {
  const notifications = useSelector(appStore, (s) => s.notifications)
  return (
    <ul>
      {notifications.map((msg, i) => <li key={i}>{msg}</li>)}
    </ul>
  )
}

function App() {
  return (
    <div>
      <Header />
      <button onClick={() => actions.login('张三', 'admin')}>登录</button>
      <button onClick={actions.logout}>登出</button>
      <button onClick={() => actions.addNotification('新消息')}>加通知</button>
      <NotificationList />
    </div>
  )
}

Store 定义在组件外(全局),任何组件都能 useSelector 订阅。状态变化时只有订阅了对应部分的组件重渲染。

37.10 常见坑

坑一:直接改 store.state 的属性。 store.state.name = '新名字' 不会触发更新。必须用 setState 返回新对象:store.setState((s) => ({ ...s, name: '新名字' }))

坑二:选择器返回新对象导致无限重渲染。 useSelector(store, (s) => ({ a: s.a, b: s.b })) 每次返回新对象,引用变了就重渲染。加 { shallow: true } 选项做浅比较。

坑三:在组件内创建 Store。 const store = createStore(0) 写在组件函数体内,每次渲染都创建新 Store,状态全丢。Store 要在组件外创建,或用 useMemo / useState 包一层。

坑四:忘了取消订阅导致内存泄漏。useEffectsubscribe 后,清理函数里要 unsubscribe()。在 React 里用 useSelector 不用操心这个,它内部自动管理。

坑五:Store 和 atom 混用导致混乱。 同一个状态一会儿用 Store 一会儿用 atom,API 不一致容易出 bug。项目里统一选一种方式。

37.11 小结

这一章你认识了 TanStack Store:

  • createStore:创建状态容器,state 读值,setState 更新,subscribe 订阅变化。
  • 派生 StorecreateStore(() => 依赖状态计算) 创建自动更新的派生状态。
  • useSelector:React 中订阅 Store 状态,用选择器精确订阅,避免不必要的重渲染。
  • batch:批量更新,多次 setState 只通知一次。
  • atom:v0.11 引入的新机制,useAtom 返回 [值, setter],体验类似 useState
  • 生态角色:Store 是 TanStack 各库的底层状态引擎,Query/Router/Form/Table 内部都在用。

下一章是整个教程的最后一章,讲 TanStack DB 和 Pacer 这两个新库,以及生态展望。