跳过正文
  1. 文章/

Zustand vs React Query vs AsyncStorage: 数据该放在哪里

· loading · loading ·
仁才德
作者
仁才德
居住在韩国首尔的领导者和软件工程师
目录

在 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>
  )
}

你免费获得的能力
#

  • 自动去重:五个组件请求同一个用户?只发一个网络请求。
  • 后台重新拉取:窗口重新获得焦点或网络重连时自动重新拉取数据。
  • 缓存管理:过期数据自动被垃圾回收。
  • 加载和错误状态:每个查询都提供 isLoadingisErrordata 等状态。
  • 重试逻辑:失败的请求会以指数退避策略自动重试。
  • 乐观更新:在服务器确认之前更新 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 来处理更大的数据集。

PlatformBackendSize LimitLocation
AndroidSQLite (RKStorage)~6 MB default (configurable)App internal storage
iOSNSUserDefaults / filesNo hard limit (keep values small)App sandbox container
WeblocalStorage~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-storereact-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',
    },
  },
})

三种网络模式:

ModeBehavior
online (default)Queries only fire when online. Pauses when offline.
alwaysQueries fire regardless of network. Your queryFn handles failures.
offlineFirstQueries 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 通过 useMutationonMutate 来实现乐观更新:

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)  │        │            │
     └────────────┘        └────────────┘
LayerToolRole
Network detectionZustand + NetInfoTrack online/offline, drive UI banners
Data fetchingReact QueryFetch when online, serve cache when offline
Cache persistenceReact Query + AsyncStorageRestore cache on app restart
Offline writesReact Query mutationsQueue mutations, replay on reconnect
Optimistic UIReact Query onMutateShow changes immediately, roll back on failure
App focus syncReact Query focusManagerRefetch 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% - 协同编辑、多设备同步、重度离线工作流 - 需要专门的同步数据库。

决策框架
#

不确定某个状态该放在哪里?问自己这些问题:

QuestionYes → 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

常见模式
#

StateToolWhy
API response dataReact QueryCaching, dedup, refetch, loading states
Selected tab / active filterZustandClient-only, multiple components care
Auth tokenAsyncStorage + ZustandPersists across restarts, fast in-memory access
Theme preferenceZustand with persist middlewareClient state that should survive restarts
Shopping cartZustand with persist middlewareComplex client logic, should survive restarts
Form inputuseStateSingle component, no need to share
User profile from APIReact QueryServer state, might be stale
Onboarding completed flagAsyncStorageJust a boolean that persists
Offline cached feedReact Query + AsyncStorageFetch 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-keychain

Expo
#

AsyncStorage 在 Expo 中开箱即用,不需要原生链接。

npx expo install @react-native-async-storage/async-storage

纯 Web React 应用
#

Web 端不需要 AsyncStorage。直接使用 localStorageIndexedDB

npm install zustand @tanstack/react-query
# 可选,用于 IndexedDB
npm install idb-keyval

Zustand 的持久化中间件原生支持 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。最好的架构则在服务器状态、客户端状态和需要持久化的数据之间画出清晰的界限。尽早搞清楚这一点,后面一切都会变得更简单。