Chapter 04

状态管理

从 useState 到 Zustand,从 Context 到 React Query。理解客户端状态与服务端状态的本质区别,选择合适的工具。

状态管理的演变

React 生态的状态管理方案经历了激烈的演变:早期 Redux 一统天下,但样板代码繁多;MobX 以响应式编程简化了写法;Recoil 尝试原子化状态;Zustand 以极简 API 赢得青睐;而 React Query 的出现让人们意识到:大多数"全局状态"其实根本不是客户端状态,而是服务端状态。

在 React Native 应用中,状态可以分为两大类,它们有根本性的不同,需要用不同工具管理:

客户端状态

  • 当前用户信息(已登录)
  • UI 状态(Modal 是否打开)
  • 主题设置(深色/浅色)
  • 购物车商品列表
  • 表单输入值

服务端状态

  • 帖子列表(来自 API)
  • 用户详情(来自 API)
  • 评论数据(来自 API)
  • 搜索结果
  • 通知列表

核心名词解释

Local State(局部状态)
组件内部的状态,用 useState 或 useReducer 管理,只有该组件和其子组件能访问。大多数 UI 状态都应该是局部状态。
Global State(全局状态)
需要在多个不相关组件间共享的客户端状态。如当前登录用户、应用主题。用 Context 或 Zustand 管理。
Server State(服务端状态)
存储在服务器上、通过 API 获取的异步数据。有缓存、过期、重新获取等概念。用 React Query 或 SWR 管理,而不是 Redux/Zustand。
useReducer
useState 的升级版,适合有多个子值或下一个状态依赖上一个状态的复杂逻辑。通过 dispatch action 触发状态变更,便于测试和调试。
Context
React 内置的跨组件状态共享机制。缺点:Context 值改变时,所有消费该 Context 的组件都会重渲染,需要配合 memo 或拆分 Context 避免性能问题。
Zustand
轻量级全局状态库(~1KB),无需 Provider 包裹,用选择器(selector)精确订阅所需状态,避免不必要的重渲染。API 极简,适合替代 Redux。
React Query(TanStack Query)
服务端状态管理库,提供缓存、后台同步、错误重试、分页等开箱即用功能。核心 Hook:useQuery(读取)、useMutation(写入)。
Stale-While-Revalidate
React Query 的缓存策略:先返回缓存数据(stale,即便过期也立即展示),同时在后台重新获取(revalidate)最新数据,更新后再渲染。极大改善用户体验。

状态流向图

React Native 状态管理全景 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌───────────────────────────────────────────┐ │ UI 组件层 │ │ │ │ [本地状态] useState / useReducer │ │ ↕ 组件内部,不需要外部库 │ │ │ │ [全局 UI 状态] Zustand Store │ │ ↓ dispatch action │ │ ↑ useStore(selector) 精确订阅 │ │ │ │ [服务端状态] React Query │ │ ↓ useQuery / useMutation │ │ ↑ 缓存数据,自动后台刷新 │ └───────────────────────────────────────────┘ ↕ ↕ ┌──────────────┐ ┌──────────────────────┐ │ Zustand Store │ │ TanStack Query │ │ (客户端全局) │ │ Cache(服务端状态) │ │ │ │ │ │ authUser │ │ posts: [...], │ │ theme │ │ user/123: {...}, │ │ cartItems │ │ search?q=react: [] │ └──────────────┘ └──────────────────────┘ ↕ ┌──────────────────┐ │ Axios / fetch │ │ API Server │ └──────────────────┘

Zustand:轻量全局状态

Zustand 是目前 React Native 社区最受欢迎的全局状态库之一。它的核心优势是:无需 Provider 包裹,任何组件都可以直接访问 Store;选择器机制确保只有订阅的状态变化时才重渲染。

// store/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface User {
  id: string;
  name: string;
  avatar: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isLoggedIn: boolean;
  login: (user: User, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  // persist 中间件:自动持久化到 AsyncStorage
  persist(
    (set) => ({
      user: null,
      token: null,
      isLoggedIn: false,
      login: (user, token) => set({ user, token, isLoggedIn: true }),
      logout: () => set({ user: null, token: null, isLoggedIn: false }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// 使用:只订阅 user,user 变化才重渲染,token 变化不触发
function ProfileHeader() {
  const user = useAuthStore(state => state.user); // 精确订阅
  const logout = useAuthStore(state => state.logout);

  if (!user) return null;
  return (
    <View>
      <Text>{user.name}</Text>
      <Pressable onPress={logout}><Text>退出</Text></Pressable>
    </View>
  );
}

React Query:服务端状态管理

// App.tsx — 配置 QueryClient
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5分钟内不重新请求
      retry: 2,                       // 失败重试2次
      refetchOnWindowFocus: false,   // App 切回前台时不自动刷新
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* 导航和其他内容 */}
    </QueryClientProvider>
  );
}

// hooks/usePosts.ts — 封装数据请求
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api';

export function usePosts() {
  return useQuery({
    queryKey: ['posts'],           // 缓存键,唯一标识这份数据
    queryFn: () => api.getPosts(),
  });
}

export function useLikePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => api.likePost(postId),
    // 乐观更新:先修改本地缓存,成功后服务器已同步,失败则回滚
    onMutate: async (postId) => {
      await queryClient.cancelQueries({ queryKey: ['posts'] });
      const prev = queryClient.getQueryData(['posts']);
      queryClient.setQueryData(['posts'], (old: Post[]) =>
        old.map(p => p.id === postId ? { ...p, likes: p.likes + 1 } : p)
      );
      return { prev }; // 快照,用于回滚
    },
    onError: (_, __, ctx) => {
      if (ctx?.prev) queryClient.setQueryData(['posts'], ctx.prev);
    },
  });
}
90% 定律 在真实项目中,90% 以上的"全局状态需求"其实是服务端状态(帖子列表、用户信息、通知等)。把这些用 React Query 管理后,Zustand 只需要处理少量真正的客户端全局状态(当前登录用户、主题、语言设置),大量简化状态管理复杂度。
useReducer 的适用场景 当组件有 3 个以上相关状态,或状态更新逻辑复杂(如表单验证),才考虑用 useReducer 替代 useState。不要为了"看起来更专业"就到处用 useReducer,useState 更简单更直观。