↓ 跳过正文

React React Native Zustand

Zustand vs React Query vs AsyncStorage:状态该放在哪里

聊聊 Zustand、React Query 和 AsyncStorage 各自该管什么,以及怎么把三者组合起来,给 React Native 应用做出真正能用的离线模式。

我正在做的窗帘报价 App 有一条硬性要求:没信号也得能用。报价从上门量尺开始,而现场经常是毛坯房或者地下室,信号根本靠不住。正是这条约束,逼着我认真回答一个大多数 React 项目都含糊对付的问题:每种状态到底该放在哪里?

最常见的翻车方式,是把所有东西塞进同一个 store。API 缓存、用户偏好、表单状态、认证令牌,全搅在同一个 Redux 大泥球里。可这些状态的性质和生命周期各不相同,各有各的合适工具。Zustand、React Query、AsyncStorage 每个都恰好把一个问题解决得很好。边界画清楚了,架构基本就自己理顺了。

状态分三种
#

聊库之前,先弄清楚自己到底在管理什么。

客户端状态只存在于应用运行时:UI 开关、选中的标签页、表单输入、弹窗开没开。上游没有谁拥有它,进程一结束它就没了。

服务器状态不一样。它的主人是服务器,应用手里只有一份本地副本。用户资料、商品列表、通知、动态流。你的副本会过期,需要重新拉取,而且此刻别的客户端可能正看着另一个版本。

持久化状态是那些必须扛过重启的数据:认证令牌、引导完成标记、缓存的偏好、离线数据。它存在设备本身上。

大多数应用三种都有。大多数烂摊子,都是拿一个工具硬管三种状态造成的。

Zustand:管客户端状态
#

Zustand 是个没什么仪式感的小状态库。没有 provider,没有模板代码,没有一层套一层的 context。建个 store,组件里直接用,要学的基本就这些。

它适合的场景:多个组件共享同一份 UI 状态(侧边栏开关、当前筛选条件、选中的项目);不来自 API 的应用级状态(主题、语言、启动时读的功能开关);以及真正的客户端逻辑,比如购物车计算或多步表单向导。凡是需要同步、可预测地更新的,基本都归它。

一个基础的 store:

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 里放的东西
#

首先是 API 响应。我见过这样的项目:每次 API 调用都写进 Zustand store,组件不查询接口,改从 store 里读。结局就是自己手搓缓存失效、加载状态、错误处理、重新拉取和分页,而这些恰恰是 React Query 白送的。

其次是需要扛过重启的数据。Zustand 的状态在内存里,应用一关就没了。persist 中间件可以搭桥到 AsyncStorage(后面会讲),但那应该是想清楚之后的决定,而不是顺手为之。

React Query:管服务器状态
#

React Query(现在叫 TanStack 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 Native 上也确实应该做,见下面的离线部分),但数据的真实来源永远是服务器。

AsyncStorage:管扛过重启的数据
#

AsyncStorage 是 React Native 的键值存储。Web 上对应的是 localStorage(同步)或 IndexedDB(异步、能力更强)。思路都一样:写到设备上,让数据活得比进程久。

各平台底下是什么
#

用 @react-native-async-storage/async-storage 的话 API 只有一套,但底下的实现每个平台差别不小。

Android 上是走 RKStorage 的 SQLite,数据库放在应用内部存储目录里。快、可靠、有沙盒隔离,别的应用碰不到。不过要留意下表里的默认容量上限。

iOS 上,小值走 NSUserDefaults,大值走序列化文件,同样被沙盒在应用容器里。Apple 没给 NSUserDefaults 定硬上限,但单个值控制在几百 KB 以内是共识。真有那么大的值,八成该直接上 WatermelonDB 或 Realm 这种正经数据库了。

Web 上(React Native Web / Expo Web)会退回 localStorage,上限看浏览器,大概 5-10 MB。纯 Web 的 React 应用直接用 localStorage 就行,数据大了就通过 idb-keyval 这类封装用 IndexedDB。

平台底层实现容量上限存储位置
AndroidSQLite (RKStorage)默认约 6 MB(可配置)应用内部存储
iOSNSUserDefaults / 文件无硬上限(单个值尽量小)应用沙盒容器
WeblocalStorage约 5-10 MB(因浏览器而异)浏览器按源(origin)隔离的存储

我放进去的东西
#

认证令牌和会话数据、需要留下来的用户偏好(语言、主题、通知设置)、引导完成标记、离线用的缓存数据。一句话概括:需要扛过重启的小型键值数据,这个类别就这么大。

API 大概是存储方案里最朴素的样子:

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') ?? '{}')

数据集大了用 IndexedDB:

import { get, set, del } from 'idb-keyval'

await set('large-dataset', hugeArray)
const data = await get('large-dataset')
await del('large-dataset')

它不适合干什么
#

它是键值存储,不是数据库。关系型数据、几千上万条的数组、需要索引和查询的东西,归 SQLite(走 expo-sqlite)、WatermelonDB 或 Realm 管。

它也不是安全存储。设备一旦被 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 会自动拉取用户资料
  }
}

AsyncStorage 负责让令牌扛过重启,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 管,persist 中间件在背后同步到 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,分工就这么简单。

离线模式,一步一步来
#

这部分才是我真正关心的。通常的说法是用户会在地铁、电梯、飞机上打开应用,这没错。但我的情况更直接:报价就是在现场写的,而现场有没有信号全看运气。地下室里转圈的加载动画,意味着丢掉一单报价。

好消息是,就靠这三个库,不用上重型框架也能搭出相当扎实的离线方案。

第一步:知道自己断网了
#

设备得把网络状态告诉应用,应用得有个地方存这个答案。事件由 @react-native-community/netinfo 提供,再用一个小小的 Zustand store 让全应用都能读到。

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',
    },
  },
})

三种模式:

模式行为
online(默认)只在联网时发起查询,断网时暂停。
always不管网络状态都发起查询,失败由你的 queryFn 处理。
offlineFirst先发起一次查询(取缓存数据),之后暂停,等联网再重新拉取。

大多数移动应用选 offlineFirst 就对了:缓存数据立刻出现,新数据等网络允许时再来。

第三步:持久化查询缓存
#

默认缓存只在内存里,一重启就满屏转圈。要做离线模式,得把缓存持久化到 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' 之后,mutationFn 会暂停到网络恢复为止。乐观更新让评论立刻出现在屏幕上,真正的 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 基本自己包了:暂停的变更恢复执行,过期的查询重新拉取。你要做的只是把它的管理器接到 React Native 的事件上。

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)  │        │            │
     └────────────┘        └────────────┘
层工具职责
网络检测Zustand + NetInfo跟踪在线/离线状态,驱动 UI 横幅
数据拉取React Query在线时拉取,离线时返回缓存
缓存持久化React Query + AsyncStorage应用重启时恢复缓存
离线写入React Query mutations变更排队,重连后重放
乐观 UIReact Query onMutate立即显示更改,失败时回滚
前台同步React Query focusManager应用回到前台时重新拉取过期数据

什么时候需要真正的同步引擎
#

上面这套的前提是:服务器是数据的真实来源,离线只是暂时状态。如果你要的是带冲突解决的真·离线优先,比如两台设备离线编辑同一篇笔记,这套就不够了,得上专门的同步引擎。WatermelonDB 专为 React Native 打造,底层是 SQLite,自带解决冲突的同步协议。Realm 配上 Atlas Device Sync,就是一个通过 MongoDB Atlas 自动解决冲突的完整离线优先数据库。PowerSync 是基于 SQLite 的同步层,能直接对接你现有的 Postgres 后端。要完全的控制权,也可以 Expo SQLite 加自己写同步逻辑。

Zustand + React Query + AsyncStorage 能覆盖大约 80% 的移动应用。剩下的 20%,也就是协同编辑、多设备同步、重度离线的工作流,需要专门的同步数据库。

拿不准放哪时
#

我拿不准的时候,会按这个顺序问自己:

问题是 → 用
数据来自服务器/API 吗?React Query
是多个组件共享的纯客户端 UI 状态吗?Zustand
需要扛过应用重启吗?AsyncStorage(可选配合 Zustand persist)
是敏感数据吗(令牌、密码)?expo-secure-store / react-native-keychain
是需要查询的大型结构化数据吗?SQLite / WatermelonDB / Realm
只是单个组件用的简单表单输入吗?useState

常见场景
#

状态工具原因
API 响应数据React Query缓存、去重、重新拉取、加载状态
选中的标签页 / 当前筛选条件Zustand纯客户端,多个组件都要用
认证令牌AsyncStorage + Zustand重启后仍在,内存中读取快
主题偏好Zustand + persist 中间件需要扛过重启的客户端状态
购物车Zustand + persist 中间件客户端逻辑复杂,需要扛过重启
表单输入useState只在单个组件里用,无需共享
来自 API 的用户资料React Query服务器状态,可能过期
引导完成标记AsyncStorage只是一个需要持久化的布尔值
离线缓存的动态流React Query + AsyncStorage从服务器拉取,失败时退回缓存

各平台安装
#

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
#

跳过 AsyncStorage,直接用 localStorage 或 IndexedDB。

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

Zustand 的 persist 中间件在 Web 上默认就用 localStorage:

persist(storeConfig, {
  name: 'my-store',
  // localStorage 在 Web 端是默认的 - 不需要额外配置
})

我反复见到的错误
#

把 API 数据塞进 Zustand。如果你的 action 里出现了 setUsers(apiResponse.users),先停一下。那是 React Query 的活,再往前走一步,就是把缓存失效重新发明一遍,而且发明得不会太好。

用 React Query 管客户端状态。queryFn 里没有网络请求?工具用错了。

把 AsyncStorage 当数据库。往键值存储里序列化一万条记录的数组,结局不会体面。用数据库。

令牌不加密。AsyncStorage 不是安全存储。认证令牌、API 密钥、用户凭证,请放 expo-secure-store 或平台的 keychain。

手搓持久化。如果你在挂载时手动读 AsyncStorage、每次状态一变就手动写回去,Zustand 的 persist 中间件早就把这些连同水合、序列化一起做好了,代码更少,坑也更少。


服务器数据走 React Query。客户端状态放 Zustand。要扛过重启的进 AsyncStorage,敏感的进安全存储。

我经手过的最糟的架构,无一例外都是所有东西流经一个巨型 store。好的架构里,服务器状态、客户端状态、需要持久化的东西之间有清清楚楚的线。这些线画得越早,后面的一切就越省事。