为什么 2026 年你应该选择 Zustand?
如果你最近在 React Native 项目里折腾过状态管理,你大概能体会那种"选择困难"的感觉。Redux 太重了,Context 一到复杂场景就拉胯,MobX 学习曲线又不低……说实话,2026 年的状态管理格局已经变了不少。Zustand 的周下载量突破了 200 万次,成了增长最快的 React 状态管理库。而 v5 的正式发布,算是给轻量级状态管理画上了一个"成熟"的标签。
Zustand 到底好在哪?简单说:打包体积只有约 1KB(gzip 后),不需要 Provider 包裹,原生集成了 React 18+ 的 useSyncExternalStore。对 React Native 来说,这意味着更小的包、更快的启动、更少的废渲染。
然后把 Zustand 和 MMKV(微信团队开源的高性能键值存储)搭配在一起,你就能拿到一套既快又能离线持久化的完整状态管理方案。这篇文章会从零开始,手把手带你搞定这套 2026 年最值得用的 React Native 状态管理技术栈。
环境准备与依赖安装
前置要求
- React Native 0.75+ 或 Expo SDK 52+(推荐 SDK 54)
- 新架构(New Architecture)已启用——MMKV v4 用的是 Nitro Modules,必须开 TurboModules
- TypeScript 4.5+(Zustand v5 的硬性要求)
- React 18+(v5 不再兼容更低版本了)
安装依赖
Expo 项目:
npx expo install zustand react-native-mmkv react-native-nitro-modules
npx expo prebuild
裸 React Native 项目:
npm install zustand react-native-mmkv react-native-nitro-modules
cd ios && pod install
这里有个容易踩的坑:react-native-mmkv v4 已经重构为 Nitro Module,所以 react-native-nitro-modules 是必装的。如果你用的是 Expo SDK 54(React Native 0.81),记得确认新架构已经开启。
Zustand v5 核心概念速览
创建第一个 Store
Zustand 的设计哲学很直白——"你的 Store 就是一个 Hook"。跟 Redux 不一样,不用 Provider、不用 Reducer、不用 Action 文件,一个函数就搞定:
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
在组件里用起来也特别顺手:
import { useCounterStore } from './stores/counterStore'
import { View, Text, Button } from 'react-native'
export function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return (
计数:{count}
)
}
Zustand v5 有啥变化?
说实话,v5 并没有加什么炫酷的新功能,它做的事情更像是"大扫除"——把 v4 里所有已经弃用的 API 全砍了。具体来说:
- 彻底告别 React 18 以下版本——直接用原生
useSyncExternalStore,包体积更小了 createWithEqualityFn搬家到了zustand/traditional——如果你之前用shallow做自定义比较,导入路径得改- 不支持 ES5 和 TypeScript 4.5 以下了——2026 年了,这应该不是问题
create里去掉了自定义 equalityFn 参数
如果你的项目之前用了 shallow 比较,迁移其实也不复杂:
// v4 写法(已弃用)
import { create } from 'zustand'
import { shallow } from 'zustand/shallow'
const useStore = create(myStore, shallow)
// v5 写法
import { createWithEqualityFn as create } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'
const useStore = create(myStore, shallow)
// v5 推荐写法(用 useShallow 更优雅)
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
const { bears, fishes } = useStore(useShallow((s) => ({ bears: s.bears, fishes: s.fishes })))
配置 MMKV 存储引擎
为什么选 MMKV 而不是 AsyncStorage?
这可能是我被问得最多的问题之一了。MMKV 是微信团队做的,专门为移动端优化过。跟 AsyncStorage 比,差距是肉眼可见的:
| 特性 | AsyncStorage | MMKV |
|---|---|---|
| 读写速度 | 慢 | 快约 30 倍 |
| 调用方式 | 异步(需 await) | 完全同步 |
| 加密支持 | 得自己实现 | 内置 AES 加密 |
| 启动性能 | 可能卡顿 | 几乎即时 |
| JS 线程影响 | 占用 Bridge | 通过 JSI 直调 Native |
在我们的项目里,从 AsyncStorage 切到 MMKV 之后,App 启动时间直接缩短了差不多 0.5 秒。这个提升在用户体验上是很明显的。
创建 MMKV 存储实例
先创建一个全局共享的 MMKV 实例。这里有个关键点:整个应用应该复用同一个 MMKV 实例,别反复创建新的。
// src/storage/mmkv.ts
import { createMMKV } from 'react-native-mmkv'
import { StateStorage } from 'zustand/middleware'
// 创建全局 MMKV 实例
export const storage = createMMKV({
id: 'app-storage',
})
// 创建 Zustand 兼容的 StateStorage 适配器
export const mmkvStorage: StateStorage = {
setItem: (name, value) => {
storage.set(name, value)
},
getItem: (name) => {
return storage.getString(name) ?? null
},
removeItem: (name) => {
storage.delete(name)
},
}
如果要存敏感数据(比如认证 Token),建议单独搞一个加密实例:
// src/storage/secureStorage.ts
import { createMMKV } from 'react-native-mmkv'
import { StateStorage } from 'zustand/middleware'
export const secureStorage = createMMKV({
id: 'secure-storage',
encryptionKey: 'your-encryption-key-here',
})
export const secureMmkvStorage: StateStorage = {
setItem: (name, value) => {
secureStorage.set(name, value)
},
getItem: (name) => {
return secureStorage.getString(name) ?? null
},
removeItem: (name) => {
secureStorage.delete(name)
},
}
构建持久化 Store:完整实战
认证 Store(加密持久化)
认证状态大概是最常见的需要持久化的场景了——没有人想每次打开 App 都重新登录一遍,对吧?
// src/stores/authStore.ts
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { secureMmkvStorage } from '../storage/secureStorage'
interface User {
id: string
name: string
email: string
avatar?: string
}
interface AuthState {
token: string | null
refreshToken: string | null
user: User | null
isAuthenticated: boolean
login: (token: string, refreshToken: string, user: User) => void
logout: () => void
updateUser: (updates: Partial) => void
}
export const useAuthStore = create()(
persist(
(set) => ({
token: null,
refreshToken: null,
user: null,
isAuthenticated: false,
login: (token, refreshToken, user) =>
set({
token,
refreshToken,
user,
isAuthenticated: true,
}),
logout: () =>
set({
token: null,
refreshToken: null,
user: null,
isAuthenticated: false,
}),
updateUser: (updates) =>
set((state) => ({
user: state.user ? { ...state.user, ...updates } : null,
})),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => secureMmkvStorage),
}
)
)
购物车 Store(配合 Immer 中间件)
一旦涉及嵌套对象的更新,不可变数据操作就会变得很啰嗦。这时候 immer 中间件就该登场了——它让你可以用"直接修改"的写法,底层自动处理不可变更新:
// src/stores/cartStore.ts
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
import { mmkvStorage } from '../storage/mmkv'
interface CartItem {
id: string
name: string
price: number
quantity: number
image?: string
}
interface CartState {
items: CartItem[]
totalPrice: number
addItem: (item: Omit) => void
removeItem: (id: string) => void
updateQuantity: (id: string, quantity: number) => void
clearCart: () => void
}
const calculateTotal = (items: CartItem[]) =>
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
export const useCartStore = create()(
persist(
immer((set) => ({
items: [],
totalPrice: 0,
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id)
if (existing) {
existing.quantity += 1
} else {
state.items.push({ ...item, quantity: 1 })
}
state.totalPrice = calculateTotal(state.items)
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id)
state.totalPrice = calculateTotal(state.items)
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((i) => i.id === id)
if (item) {
item.quantity = Math.max(0, quantity)
if (item.quantity === 0) {
state.items = state.items.filter((i) => i.id !== id)
}
}
state.totalPrice = calculateTotal(state.items)
}),
clearCart: () =>
set((state) => {
state.items = []
state.totalPrice = 0
}),
})),
{
name: 'cart-storage',
storage: createJSONStorage(() => mmkvStorage),
partialize: (state) => ({
items: state.items,
totalPrice: state.totalPrice,
}),
}
)
)
主题 Store(轻量持久化)
不是所有 Store 都需要那么复杂。主题切换就是个典型的"简单但需要持久化"的场景:
// src/stores/themeStore.ts
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { mmkvStorage } from '../storage/mmkv'
type ThemeMode = 'light' | 'dark' | 'system'
interface ThemeState {
mode: ThemeMode
setMode: (mode: ThemeMode) => void
}
export const useThemeStore = create()(
persist(
(set) => ({
mode: 'system',
setMode: (mode) => set({ mode }),
}),
{
name: 'theme-storage',
storage: createJSONStorage(() => mmkvStorage),
}
)
)
异步操作与服务端状态
在 Store 中处理 API 请求
Zustand 在异步方面做得很"偷懒"(褒义的)——直接在 action 里写 async/await 就行,不需要任何额外的中间件。对比一下 Redux 那套 thunk 或 saga 的仪式感,这简直太清爽了:
// src/stores/productStore.ts
import { create } from 'zustand'
interface Product {
id: string
name: string
price: number
description: string
}
interface ProductState {
products: Product[]
isLoading: boolean
error: string | null
fetchProducts: () => Promise
fetchProductById: (id: string) => Promise
}
export const useProductStore = create()((set, get) => ({
products: [],
isLoading: false,
error: null,
fetchProducts: async () => {
set({ isLoading: true, error: null })
try {
const response = await fetch('https://api.example.com/products')
const products = await response.json()
set({ products, isLoading: false })
} catch (error) {
set({
error: error instanceof Error ? error.message : '获取商品失败',
isLoading: false,
})
}
},
fetchProductById: async (id) => {
const cached = get().products.find((p) => p.id === id)
if (cached) return cached
try {
const response = await fetch(
`https://api.example.com/products/${id}`
)
return await response.json()
} catch {
return null
}
},
}))
更推荐的架构:Zustand + TanStack Query
不过话说回来,如果你的应用有大量的 API 交互,我其实更建议把客户端状态和服务端状态分开管理:
- Zustand:管客户端本地状态(UI 状态、用户偏好、购物车这些)
- TanStack Query:管服务端状态(API 数据、缓存、分页这些)
这样分层的好处是,你不用在 Zustand 里维护一堆 isLoading、error 状态,同时还能白嫖 TanStack Query 的自动缓存、后台刷新和乐观更新。实际做下来,代码量大概能减少三分之一。
性能优化:选择器与重渲染控制
用选择器精确订阅状态切片
这是 Zustand 最核心的性能优化手段了——选择器(Selector)。通过选择器,组件只在它关心的那部分状态变了才会重渲染:
// ❌ 错误做法:订阅整个 Store,任何状态变化都触发重渲染
const state = useCartStore()
// ✅ 正确做法:只订阅需要的字段
const items = useCartStore((state) => state.items)
const totalPrice = useCartStore((state) => state.totalPrice)
// ✅ 更优做法:用 useShallow 同时订阅多个字段
import { useShallow } from 'zustand/shallow'
const { items, totalPrice } = useCartStore(
useShallow((state) => ({
items: state.items,
totalPrice: state.totalPrice,
}))
)
小心无限循环
这是个容易踩的坑。在 Zustand v5 里,如果选择器每次都返回新的引用(比如 filter 产生新数组),可能会触发 Maximum update depth exceeded 错误。用 useShallow 做浅比较就能解决:
// ❌ 可能导致无限循环——每次渲染都返回新数组引用
const items = useCartStore((state) => state.items.filter((i) => i.quantity > 0))
// ✅ 用 useShallow 避免不必要的重渲染
const items = useCartStore(
useShallow((state) => state.items.filter((i) => i.quantity > 0))
)
处理 Hydration:解决启动闪烁
用了 MMKV 持久化之后,App 启动时有个短暂的状态恢复过程(Hydration)。如果你不处理,用户可能会先看到登录页面,然后突然跳到首页——这体验就很糟糕了。
用 onRehydrateStorage 回调
// src/stores/authStore.ts(增加 hydration 处理)
export const useAuthStore = create()(
persist(
(set) => ({
// ... 状态和 actions
_hasHydrated: false,
setHasHydrated: (state: boolean) => set({ _hasHydrated: state }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => secureMmkvStorage),
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true)
},
}
)
)
做一个启动守卫组件
思路很简单:Hydration 没完成之前,显示一个加载动画;完成了再渲染正式内容。
// src/components/HydrationGate.tsx
import { useAuthStore } from '../stores/authStore'
import { ActivityIndicator, View, StyleSheet } from 'react-native'
interface HydrationGateProps {
children: React.ReactNode
}
export function HydrationGate({ children }: HydrationGateProps) {
const hasHydrated = useAuthStore((state) => state._hasHydrated)
if (!hasHydrated) {
return (
)
}
return <>{children}>
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})
调试与 DevTools
配置 DevTools 中间件
Zustand 能接入 Redux DevTools,配合 Flipper 在 React Native 里做状态调试还是很方便的:
// src/stores/cartStore.ts(添加 devtools)
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
export const useCartStore = create()(
devtools(
persist(
immer((set) => ({
// ... Store 定义
})),
{
name: 'cart-storage',
storage: createJSONStorage(() => mmkvStorage),
}
),
{ name: 'CartStore' }
)
)
中间件嵌套顺序很重要,这里给个经验:devtools 包最外面,persist 在中间,immer 在最里面。搞反了的话,DevTools 可能抓不到 action,或者持久化出问题。(我在这上面踩过坑,调了半天才发现是顺序不对。)
给 Action 加标签
调试的时候,给每个 set 操作起个名字会让你轻松很多:
addItem: (item) =>
set(
(state) => {
// ... 更新逻辑
},
false,
'cart/addItem'
),
生产级项目架构建议
推荐的目录结构
src/
├── storage/
│ ├── mmkv.ts # 通用 MMKV 实例
│ └── secureStorage.ts # 加密 MMKV 实例
├── stores/
│ ├── authStore.ts # 认证状态
│ ├── cartStore.ts # 购物车状态
│ ├── themeStore.ts # 主题偏好
│ └── index.ts # 统一导出
├── components/
│ └── HydrationGate.tsx # Hydration 守卫
└── hooks/
└── useStoreSelectors.ts # 常用选择器封装
Store 设计六原则
这些是我在几个生产项目里总结出来的经验,希望能帮你少走弯路:
- 按领域拆分 Store——每个 Store 管一个业务领域(认证、购物车、偏好设置等)。千万别搞一个巨型全局 Store,那是灾难的开始
- 永远用选择器——不要直接订阅整个 Store,否则性能优势就白费了
- 只持久化必要数据——用
partialize排除临时状态(比如 isLoading),别什么都往 MMKV 里塞 - 敏感数据加密存储——Token、用户凭证这些,用带
encryptionKey的独立 MMKV 实例 - 分离客户端与服务端状态——Zustand 管本地的,TanStack Query 管 API 的
- 中间件按需加——不是每个 Store 都需要 persist / immer / devtools 全套,够用就好
Zustand vs Redux vs Jotai:怎么选?
最后聊聊选型的问题,这个因项目而异,没有绝对正确的答案:
| 场景 | 推荐方案 |
|---|---|
| 新项目或中小团队 | Zustand——API 简单,上手快,维护成本低 |
| 大型企业项目或已有 Redux 代码 | Redux Toolkit——生态成熟,可预测性强 |
| 复杂原子级状态依赖 | Jotai——细粒度响应式更新 |
| 想要自动追踪的简洁方案 | Valtio——Proxy 驱动,写起来最"自然" |
常见问题
Zustand 在 React Native 里性能到底怎么样?
实测下来,Zustand 比 Redux 快不少。在 1000 个组件订阅状态的基准测试中,Zustand 单次状态更新大约 12ms,Redux Toolkit 需要 18ms。包体积方面差距更大:Zustand 只有 1-3KB(gzip),Redux Toolkit 加 react-redux 差不多 15KB。得益于细粒度选择器,高频更新场景下 Zustand 的响应速度比 Redux 快约 40%。
MMKV 和 AsyncStorage 到底差在哪?
最大的区别是速度和调用方式。MMKV 读写快约 30 倍,而且是同步的,不需要 async/await。它还内置了 AES 加密,通过 JSI 直接调原生层不走 Bridge。AsyncStorage 对于简单场景还行,但如果你对性能有要求,MMKV 是更好的选择。不过 MMKV v4 要求新架构,这点需要注意。
升级到 Zustand v5 要改多少代码?
看你之前用了多少"被弃用"的 API。最常见的改动就是 shallow 比较函数的迁移——要么改用 zustand/traditional 的 createWithEqualityFn,要么(推荐)换成 useShallow Hook。另外需要确保 React 18+ 和 TypeScript 4.5+。我的建议是先把 v4 升到最新,看看弃用警告,然后再切 v5。
persist 中间件在 App 重启后数据会丢吗?
不会。配合 MMKV 之后,数据会持久化到设备本地存储,App 重启自动恢复。但有个注意事项:别把整个 Store 都持久化,用 partialize 只存关键字段。大量结构化数据应该用 SQLite,极度敏感的东西(比如密钥)用系统的 Keychain 或 Keystore。
启动时状态闪烁怎么解决?
这个问题本质上是 Hydration 需要时间。解决方案是用 onRehydrateStorage 回调设一个 _hasHydrated 标志,然后做一个 HydrationGate 组件——Hydration 没完成就显示加载动画,完成了再渲染真正的内容。上面的代码示例里有完整实现,直接拿去用就行。