在 React 和 React Native 项目中,最常见的错误之一就是把所有东西都塞进一个状态管理方案里。结果你的 Redux store 同时处理 API 缓存、用户偏好、表单状态和认证令牌,搅成一团乱麻。这些是不同类型的状态,需要不同的工具来处理。
Zustand、React Query 和 AsyncStorage 各自解决不同的问题。一旦你理解了它们之间的边界,你的架构会干净很多。
三种类型的状态#
在选工具之前,先搞清楚你到底在管理什么。
客户端状态是只存在于应用运行时的数据。比如 UI 开关、选中的标签页、表单输入、弹窗的显示/隐藏、主题偏好。它只在应用运行期间存在,不来自服务器。
服务器状态是存储在远程服务器上的数据,你的应用只有一份本地副本。用户资料、商品列表、通知、动态流。它可能会过期,需要重新拉取,而且多个客户端可能看到不同的版本。
持久化状态是需要在应用重启后依然保留的数据。认证令牌、引导流程完成标记、缓存的用户偏好、离线数据。它存储在设备本身。
大多数应用都有这三种状态。错误在于用一个工具搞定一切。
Zustand: 正确处理客户端状态#
Zustand 是一个轻量级的 React 状态管理库。没有 provider,没有模板代码,没有 context 嵌套地狱。你创建一个 store,在组件里使用它,就这样。
什么时候使用 Zustand#
- 多个组件需要共享的 UI 状态(侧边栏的开/关、当前筛选条件、已选中的项目)
- 不来自 API 的应用级状态(主题、语言、启动时加载的功能开关)
- 复杂的客户端逻辑(购物车计算、多步骤表单向导)
- 需要同步且可预测地更新的状态
基础示例#
import { create } from 'zustand'
interface AppState {
theme: 'light' | 'dark'
sidebarOpen: boolean
selectedFilters: string[]
setTheme: (theme: 'light' | 'dark') => void
toggleSidebar: () => void
setFilters: (filters: string[]) => void
}
const useAppStore = create<AppState>((set) => ({
theme: 'light',
sidebarOpen: false,
selectedFilters: [],
setTheme: (theme) => set({ theme }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setFilters: (filters) => set({ selectedFilters: filters }),
}))在组件中使用:
function Sidebar() {
const { sidebarOpen, toggleSidebar } = useAppStore()
if (!sidebarOpen) return null
return (
<div className="sidebar">
<button onClick={toggleSidebar}>Close</button>
{/* sidebar content */}
</div>
)
}Zustand 不适合做什么#
不要用 Zustand 缓存 API 响应。我见过有的项目把每个 API 调用的结果都写进 Zustand store,然后组件从 store 读数据而不是查询 API。你最终会自己重新实现缓存失效、加载状态、错误处理、重新拉取和分页 - 这些 React Query 都是免费提供的。
不要用 Zustand 存储需要在应用重启后保留的数据。Zustand 的状态存在内存里,应用关闭就没了。你可以添加持久化中间件(后面会讲),但到那时你其实已经进入了 AsyncStorage 的领域,应该想清楚再做。
React Query: 告别服务器状态的痛苦#
React Query(TanStack Query)管理远程数据的整个生命周期。拉取、缓存、同步、更新和垃圾回收。它把服务器数据当作需要保持新鲜的缓存,而不是你拥有的状态。
什么时候使用 React Query#
- 任何来自 API 的数据
- 多个组件需要同一个接口的数据(它会自动去重请求)
- 分页或无限滚动数据
- 需要后台重新拉取的数据(用户回到应用时,数据可能已经过期)
- 乐观更新(立即更新 UI,如果服务器拒绝则回滚)
基础示例#
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 分钟内视为新鲜
})
}
function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { id: string; name: string }) =>
fetch(`/api/users/${data.id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user', variables.id] })
},
})
}在组件中使用:
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId)
const updateUser = useUpdateUser()
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return (
<div>
<h1>{user.name}</h1>
<button
onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}
disabled={updateUser.isPending}
>
Update Name
</button>
</div>
)
}你免费获得的能力#
- 自动去重:五个组件请求同一个用户?只发一个网络请求。
- 后台重新拉取:窗口重新获得焦点或网络重连时自动重新拉取数据。
- 缓存管理:过期数据自动被垃圾回收。
- 加载和错误状态:每个查询都提供
isLoading、isError、data等状态。 - 重试逻辑:失败的请求会以指数退避策略自动重试。
- 乐观更新:在服务器确认之前更新 UI,失败时回滚。
React Query 不适合做什么#
不要用 React Query 管理纯客户端状态。如果数据不是来自服务器,React Query 就是在增加不必要的复杂度。一个弹窗的打开/关闭状态不需要缓存失效或重新拉取间隔。
不要把 React Query 当作长期持久化层。它的缓存存在内存里。应用重启后缓存就空了,所有数据都会重新拉取。你可以持久化缓存(React Native 应用确实应该这么做),但数据的真实来源始终是服务器。
AsyncStorage: 跨平台的持久化状态#
AsyncStorage 是 React Native 的键值对存储方案。在 Web 上,对应的是 localStorage(同步)或 IndexedDB(异步,功能更强)。同样的思路 - 通过存储在设备上来让数据在应用重启后依然保留。
各平台实现#
每个平台的存储后端不同,但如果你用的是 @react-native-async-storage/async-storage,API 是一样的:
Android:底层使用通过 RKStorage 实现的 SQLite。数据存储在应用内部存储目录的 SQLite 数据库中。速度快、可靠,没有实际的大小限制(不过 Android 可能会对每个应用的存储施加限制)。数据是沙盒化的 - 其他应用无法访问。
iOS:小数据使用 NSUserDefaults,大数据使用序列化的数据文件。和 Android 一样,数据在应用容器中沙盒化。Apple 没有对 NSUserDefaults 施加硬性限制,但最佳实践是让单个值保持在几百 KB 以下。对于更大的数据,考虑使用 WatermelonDB 或 Realm 之类的数据库。
Web(React Native Web / Expo Web):降级为 localStorage,根据浏览器不同有大约 5-10 MB 的限制。对于纯 Web 的 React 应用,你可以直接使用 localStorage,或者通过 idb-keyval 之类的封装库使用 IndexedDB 来处理更大的数据集。
| Platform | Backend | Size Limit | Location |
|---|---|---|---|
| Android | SQLite (RKStorage) | ~6 MB default (configurable) | App internal storage |
| iOS | NSUserDefaults / files | No hard limit (keep values small) | App sandbox container |
| Web | localStorage | ~5-10 MB (browser-dependent) | Browser origin storage |
什么时候使用 AsyncStorage#
- 认证令牌和会话数据
- 需要持久化的用户偏好(语言、主题、通知设置)
- 引导流程完成标记
- 离线优先体验的缓存数据
- 任何需要在应用重启后保留的小型键值对数据
基础示例#
import AsyncStorage from '@react-native-async-storage/async-storage'
// 存储一个值
await AsyncStorage.setItem('auth_token', token)
// 读取一个值
const token = await AsyncStorage.getItem('auth_token')
// 存储一个对象(需要序列化)
await AsyncStorage.setItem('user_preferences', JSON.stringify({
theme: 'dark',
language: 'en',
notifications: true,
}))
// 读取一个对象
const prefs = JSON.parse(await AsyncStorage.getItem('user_preferences') ?? '{}')
// 删除一个值
await AsyncStorage.removeItem('auth_token')
// 清空所有数据(慎用)
await AsyncStorage.clear()Web 端等价方案#
对于纯 Web 的 React 应用,你不需要 AsyncStorage。用 localStorage 处理简单的键值对即可:
// 同步的 - 会阻塞主线程,但对于小数据没问题
localStorage.setItem('theme', 'dark')
const theme = localStorage.getItem('theme')
// 结构化数据
localStorage.setItem('user', JSON.stringify({ name: 'Jared', role: 'admin' }))
const user = JSON.parse(localStorage.getItem('user') ?? '{}')对于 Web 端更大的数据集,使用 IndexedDB:
import { get, set, del } from 'idb-keyval'
await set('large-dataset', hugeArray)
const data = await get('large-dataset')
await del('large-dataset')AsyncStorage 不适合做什么#
不要把 AsyncStorage 当作你的主数据存储。它是键值对存储,不是数据库。如果你要存储关系型数据、上万条记录的数组,或任何需要索引和查询的数据,请使用 SQLite(通过 expo-sqlite)、WatermelonDB 或 Realm。
不要在没有加密的情况下在 AsyncStorage 中存储敏感数据。在 Android 和 iOS 上,如果设备被 root/越狱,AsyncStorage 的数据就能被访问。对于敏感令牌,请改用 expo-secure-store 或 react-native-keychain。
它们如何协同工作#
在实际应用中,这三个工具是配合使用的。
示例:用户认证流程#
// 1. AsyncStorage: 持久化认证令牌
import AsyncStorage from '@react-native-async-storage/async-storage'
async function saveToken(token: string) {
await AsyncStorage.setItem('auth_token', token)
}
async function getToken(): Promise<string | null> {
return AsyncStorage.getItem('auth_token')
}
// 2. Zustand: 在内存中跟踪认证状态
import { create } from 'zustand'
interface AuthState {
isAuthenticated: boolean
token: string | null
setAuth: (token: string) => void
clearAuth: () => void
}
const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false,
token: null,
setAuth: (token) => set({ isAuthenticated: true, token }),
clearAuth: () => set({ isAuthenticated: false, token: null }),
}))
// 3. React Query: 使用令牌获取用户资料
function useCurrentUser() {
const token = useAuthStore((s) => s.token)
return useQuery({
queryKey: ['currentUser'],
queryFn: () =>
fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` },
}).then(res => res.json()),
enabled: !!token, // 只在有令牌时才发请求
})
}应用启动时:
// 应用初始化
async function initializeApp() {
const token = await getToken() // 从 AsyncStorage 读取
if (token) {
useAuthStore.getState().setAuth(token) // 放入 Zustand 以便快速访问
// React Query 会自动拉取用户资料
}
}示例:带持久化的主题偏好#
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import AsyncStorage from '@react-native-async-storage/async-storage'
// Zustand 的持久化中间件在两者之间搭建了桥梁
const useThemeStore = create(
persist(
(set) => ({
theme: 'light' as 'light' | 'dark',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{
name: 'theme-storage',
storage: createJSONStorage(() => AsyncStorage), // React Native
// storage: createJSONStorage(() => localStorage), // Web
}
)
)Zustand 管理运行时状态,持久化中间件自动同步到 AsyncStorage(或 Web 端的 localStorage)。你的组件不需要知道也不用关心持久化层。
示例:离线优先的数据#
import { useQuery } from '@tanstack/react-query'
import AsyncStorage from '@react-native-async-storage/async-storage'
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
try {
const res = await fetch('/api/products')
const data = await res.json()
// 缓存到 AsyncStorage 以供离线使用
await AsyncStorage.setItem('cached_products', JSON.stringify(data))
return data
} catch (error) {
// 网络请求失败 - 尝试使用缓存数据
const cached = await AsyncStorage.getItem('cached_products')
if (cached) return JSON.parse(cached)
throw error
}
},
staleTime: 10 * 60 * 1000,
})
}React Query 处理拉取和内存缓存。AsyncStorage 提供离线兜底。
为移动应用构建离线模式#
对于移动端来说,离线支持不是可选的。用户在地铁、电梯、飞机上、信号不好的地方打开你的应用。如果你的应用每次断网就显示白屏或转圈,用户会直接走人。
Zustand、React Query 和 AsyncStorage 三者配合,不需要用重型框架就能给你一个可靠的离线架构。
第一步:检测网络状态#
首先,你需要知道设备是在线还是离线。使用 @react-native-community/netinfo 并在 Zustand 中跟踪状态,这样整个应用都能做出响应。
import { create } from 'zustand'
import NetInfo from '@react-native-community/netinfo'
interface NetworkState {
isOnline: boolean
setOnline: (online: boolean) => void
}
const useNetworkStore = create<NetworkState>((set) => ({
isOnline: true,
setOnline: (online) => set({ isOnline: online }),
}))
// 应用启动时订阅一次
NetInfo.addEventListener((state) => {
useNetworkStore.getState().setOnline(state.isConnected ?? false)
})任何组件都可以通过 useNetworkStore((s) => s.isOnline) 来检查并显示离线提示、禁用按钮,或显示"更改将在联网后同步"的消息。
第二步:为离线配置 React Query#
React Query 通过 networkMode 内置了离线支持。它控制查询和变更在设备离线时的行为。
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst',
// 先返回缓存数据,联网后在后台重新拉取
staleTime: 5 * 60 * 1000,
gcTime: 24 * 60 * 60 * 1000, // 缓存保留 24 小时
retry: (failureCount, error) => {
// 离线时不要重试 - 反正也会失败
if (!useNetworkStore.getState().isOnline) return false
return failureCount < 3
},
},
mutations: {
networkMode: 'offlineFirst',
},
},
})三种网络模式:
| Mode | Behavior |
|---|---|
online (default) | Queries only fire when online. Pauses when offline. |
always | Queries fire regardless of network. Your queryFn handles failures. |
offlineFirst | Queries fire once (for cached data), then pause until online to refetch. |
对于大多数移动应用,offlineFirst 是正确的选择。用户立即看到缓存数据,网络可用时加载最新数据。
第三步:持久化 React Query 缓存#
默认情况下,React Query 的缓存存在内存中。应用重启 = 空缓存 = 到处都是加载转圈。要实现离线模式,你需要把缓存持久化到 AsyncStorage,这样它能在重启后保留。
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import AsyncStorage from '@react-native-async-storage/async-storage'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 24 * 60 * 60 * 1000, // 24 小时 - 必须 >= maxAge
},
},
})
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'react-query-cache',
})
// 在你的 App 组件中
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: asyncStoragePersister,
maxAge: 24 * 60 * 60 * 1000, // 不恢复超过 24 小时的数据
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// 只持久化成功的查询
return query.state.status === 'success'
},
},
}}
>
<YourApp />
</PersistQueryClientProvider>
)
}用户在离线状态下打开应用时,会立即看到上次拉取的数据,而不是白屏。网络恢复后,React Query 会在后台重新拉取。
第四步:离线时排队变更操作#
离线读取缓存数据很简单。处理写入就难多了。当用户在离线状态下创建帖子、提交订单或更新个人资料时,你需要把这个变更操作排队,等网络恢复后重放。
React Query 通过 useMutation 和 onMutate 来实现乐观更新:
import { useMutation, useQueryClient } from '@tanstack/react-query'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface Comment {
id: string
text: string
postId: string
createdAt: string
pending?: boolean
}
function useAddComment(postId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (text: string) => {
const res = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
return res.json()
},
// 乐观更新 - 立即显示评论
onMutate: async (text) => {
await queryClient.cancelQueries({ queryKey: ['comments', postId] })
const previous = queryClient.getQueryData<Comment[]>(['comments', postId])
const optimisticComment: Comment = {
id: `temp-${Date.now()}`,
text,
postId,
createdAt: new Date().toISOString(),
pending: true, // 在 UI 中显示"发送中..."的标识
}
queryClient.setQueryData<Comment[]>(
['comments', postId],
(old) => [...(old ?? []), optimisticComment]
)
return { previous }
},
// 失败时回滚
onError: (err, text, context) => {
if (context?.previous) {
queryClient.setQueryData(['comments', postId], context.previous)
}
},
// 重新拉取以获取服务器的真实数据
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['comments', postId] })
},
})
}在变更操作上设置了 networkMode: 'offlineFirst' 后,React Query 会暂停 mutationFn 直到网络恢复。乐观更新会立即在 UI 中显示评论,实际的 API 调用会在设备重新连接后触发。
对于更复杂的离线队列(比如按顺序同步几十个待处理的变更),你可以持久化变更队列:
import { MutationCache } from '@tanstack/react-query'
// 将待处理的变更保存到 AsyncStorage
const mutationCache = new MutationCache({
onError: async (error, variables, context, mutation) => {
// 记录失败的变更以便调试
const pending = JSON.parse(
await AsyncStorage.getItem('pending_mutations') ?? '[]'
)
pending.push({
key: mutation.options.mutationKey,
variables,
timestamp: Date.now(),
})
await AsyncStorage.setItem('pending_mutations', JSON.stringify(pending))
},
})第五步:恢复联网后同步#
网络恢复后,你需要将本地更改与服务器对账。React Query 自动处理大部分工作 - 暂停的变更会恢复执行,过期的查询会重新拉取。但你还需要处理一些边界情况。
import NetInfo from '@react-native-community/netinfo'
import { onlineManager, focusManager } from '@tanstack/react-query'
import { AppState } from 'react-native'
// 告诉 React Query 网络状态的变化
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected)
})
})
// 应用回到前台时重新拉取
focusManager.setEventListener((setFocused) => {
const subscription = AppState.addEventListener('change', (status) => {
setFocused(status === 'active')
})
return () => subscription.remove()
})当用户把应用切到后台一小时再回来时,focusManager 会触发重新拉取,让他们看到最新数据。当他们走出隧道恢复信号时,onlineManager 会恢复暂停的变更操作。
第六步:在 UI 中显示离线状态#
不要默默吞掉网络错误。告诉用户他们处于离线状态,以及他们的更改正在等待同步。
import { useNetworkStore } from './stores/network'
function OfflineBanner() {
const isOnline = useNetworkStore((s) => s.isOnline)
if (isOnline) return null
return (
<View style={styles.banner}>
<Text>You're offline. Changes will sync when you reconnect.</Text>
</View>
)
}
function CommentItem({ comment }: { comment: Comment }) {
return (
<View style={[styles.comment, comment.pending && styles.pending]}>
<Text>{comment.text}</Text>
{comment.pending && (
<Text style={styles.pendingLabel}>Sending...</Text>
)}
</View>
)
}整合离线架构#
三个工具在离线模式中如何协同工作:
┌─────────────────────────────────────────────────┐
│ Components │
│ useQuery() for reads useMutation() for writes│
└──────────┬──────────────────────┬────────────────┘
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ React Query │ │ React Query │
│ Cache │ │ Mutation │
│ (in-memory) │ │ Queue │
└─────┬──────┘ └─────┬──────┘
│ │
┌─────▼──────────────────────▼──────┐
│ AsyncStorage Persister │
│ (survives app restart) │
└─────┬──────────────────────┬──────┘
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ Zustand │ │ Network │
│ (isOnline, │◄───────│ NetInfo │
│ UI state) │ │ │
└────────────┘ └────────────┘| Layer | Tool | Role |
|---|---|---|
| Network detection | Zustand + NetInfo | Track online/offline, drive UI banners |
| Data fetching | React Query | Fetch when online, serve cache when offline |
| Cache persistence | React Query + AsyncStorage | Restore cache on app restart |
| Offline writes | React Query mutations | Queue mutations, replay on reconnect |
| Optimistic UI | React Query onMutate | Show changes immediately, roll back on failure |
| App focus sync | React Query focusManager | Refetch stale data when app returns to foreground |
什么时候需要更重的方案#
当服务器是数据的真实来源、离线只是暂时的情况下,上面的方案运行得很好。如果你的应用需要真正的离线优先能力并具备冲突解决 - 比如一个笔记应用,两台设备在离线状态下编辑同一篇文档 - 你需要一个专门的同步引擎:
- WatermelonDB:专为 React Native 构建,底层使用 SQLite,有用于解决冲突的同步协议
- Realm (Atlas Device Sync):完整的离线优先数据库,通过 MongoDB Atlas 实现自动冲突解决
- PowerSync:基于 SQLite 的同步层,可与你现有的 Postgres 后端配合使用
- Expo SQLite + 自定义同步:如果你需要完全控制,可以自己实现
Zustand + React Query + AsyncStorage 覆盖了大约 80% 的移动应用场景。剩下的 20% - 协同编辑、多设备同步、重度离线工作流 - 需要专门的同步数据库。
决策框架#
不确定某个状态该放在哪里?问自己这些问题:
| Question | Yes → Use |
|---|---|
| Does it come from a server/API? | React Query |
| Is it client-only UI state shared across components? | Zustand |
| Does it need to survive an app restart? | AsyncStorage (+ optionally Zustand persist) |
| Is it sensitive (tokens, passwords)? | expo-secure-store / react-native-keychain |
| Is it large structured data that needs querying? | SQLite / WatermelonDB / Realm |
| Is it a simple form input used by one component? | useState |
常见模式#
| State | Tool | Why |
|---|---|---|
| API response data | React Query | Caching, dedup, refetch, loading states |
| Selected tab / active filter | Zustand | Client-only, multiple components care |
| Auth token | AsyncStorage + Zustand | Persists across restarts, fast in-memory access |
| Theme preference | Zustand with persist middleware | Client state that should survive restarts |
| Shopping cart | Zustand with persist middleware | Complex client logic, should survive restarts |
| Form input | useState | Single component, no need to share |
| User profile from API | React Query | Server state, might be stale |
| Onboarding completed flag | AsyncStorage | Just a boolean that persists |
| Offline cached feed | React Query + AsyncStorage | Fetch from server, fall back to cache |
各平台注意事项#
React Native (Android + iOS)#
安装全部三个:
npm install zustand @tanstack/react-query @react-native-async-storage/async-storage对于敏感数据,添加安全存储:
npx expo install expo-secure-store
# 或者
npm install react-native-keychainExpo#
AsyncStorage 在 Expo 中开箱即用,不需要原生链接。
npx expo install @react-native-async-storage/async-storage纯 Web React 应用#
Web 端不需要 AsyncStorage。直接使用 localStorage 或 IndexedDB。
npm install zustand @tanstack/react-query
# 可选,用于 IndexedDB
npm install idb-keyvalZustand 的持久化中间件原生支持 localStorage:
persist(storeConfig, {
name: 'my-store',
// localStorage 在 Web 端是默认的 - 不需要额外配置
})要避免的反模式#
把 API 数据放进 Zustand。 如果你在 Zustand action 里写 setUsers(apiResponse.users),停下来。用 React Query。你正在重新发明缓存失效,结果不会好。
用 React Query 管理客户端状态。 如果你的 queryFn 没有发网络请求,那你用错工具了。React Query 是给服务器状态用的。
在 AsyncStorage 中存储大量数据。 AsyncStorage 是键值对存储。如果你在序列化一万条记录的数组,用数据库。
不加密令牌。 AsyncStorage 不是安全存储。认证令牌、API 密钥和用户凭证应该放在 expo-secure-store 或平台的 keychain 中。
跳过持久化中间件。 如果你在挂载时手动从 AsyncStorage 读取数据,在每次状态变化时手动写入,那就直接用 Zustand 的持久化中间件吧。它帮你处理了水合、序列化和存储同步。
服务器数据走 React Query。客户端状态放 Zustand。持久化数据放 AsyncStorage。敏感数据放安全存储。
我经历过的最糟糕的架构,就是所有东西都流经一个巨大的 store。最好的架构则在服务器状态、客户端状态和需要持久化的数据之间画出清晰的界限。尽早搞清楚这一点,后面一切都会变得更简单。

