Store:响应式状态管理
本教程共 38 篇 · 第 37 篇 · 更新于 2026-07-27 · 约 11 分钟阅读
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)
NoteAtom 和 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 管理表格状态。
你用 useQuery、useForm、useReactTable 时,底层都在用 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 包一层。
坑四:忘了取消订阅导致内存泄漏。 在 useEffect 里 subscribe 后,清理函数里要 unsubscribe()。在 React 里用 useSelector 不用操心这个,它内部自动管理。
坑五:Store 和 atom 混用导致混乱。 同一个状态一会儿用 Store 一会儿用 atom,API 不一致容易出 bug。项目里统一选一种方式。
37.11 小结
这一章你认识了 TanStack Store:
- createStore:创建状态容器,
state读值,setState更新,subscribe订阅变化。 - 派生 Store:
createStore(() => 依赖状态计算)创建自动更新的派生状态。 - useSelector:React 中订阅 Store 状态,用选择器精确订阅,避免不必要的重渲染。
- batch:批量更新,多次
setState只通知一次。 - atom:v0.11 引入的新机制,
useAtom返回[值, setter],体验类似useState。 - 生态角色:Store 是 TanStack 各库的底层状态引擎,Query/Router/Form/Table 内部都在用。
下一章是整个教程的最后一章,讲 TanStack DB 和 Pacer 这两个新库,以及生态展望。