Expo应用中如何选择状态管理方案?Zustand、Redux Toolkit、Jotai实战对比
Expo应用的状态管理选型直接影响项目可维护性和开发效率。目前社区主流方案有Zustand、Redux Toolkit、Jotai和Context API,它们各有适用场景。下面从实际项目出发,逐一分析各方案的用法、优劣势和集成方式。
方案概览与选型依据
| 方案 | 包大小(gzip) | 心智模型 | 是否需要Provider | 适合规模 |
|---|---|---|---|---|
| Context API | 0(内置) | 树形共享 | 是 | 小型 |
| Zustand | ~3KB | 集中式Store | 否 | 中小型 |
| Redux Toolkit | ~15KB | 集中式Store | 是 | 大型 |
| Jotai | ~4KB | 原子化 | 否 | 中型 |
选型的核心判断依据:项目有多少全局状态、团队规模、是否需要时间旅行调试、以及包大小是否敏感。
Context API:小项目的零依赖方案
Context API适合主题切换、语言设置、用户登录信息等少量全局状态。不需要引入任何第三方库,但性能隐患在于:Context值变化时,所有消费该Context的组件都会重新渲染。
typescriptimport { createContext, useContext, useState, useMemo, useCallback } from 'react'; import { Text, View, Button } from 'react-native'; type UserState = { name: string; isLoggedIn: boolean; }; type UserContextType = { user: UserState; login: (name: string) => void; logout: () => void; }; const UserContext = createContext<UserContextType | null>(null); export function UserProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<UserState>({ name: '', isLoggedIn: false }); const login = useCallback((name: string) => { setUser({ name, isLoggedIn: true }); }, []); const logout = useCallback(() => { setUser({ name: '', isLoggedIn: false }); }, []); const value = useMemo(() => ({ user, login, logout }), [user, login, logout]); return <UserContext.Provider value={value}>{children}</UserContext.Provider>; } // 自定义hook封装,避免组件直接依赖Context export function useUser() { const ctx = useContext(UserContext); if (!ctx) throw new Error('useUser must be used within UserProvider'); return ctx; }
关键点:用useMemo和useCallback避免不必要的重渲染,用自定义hook封装Context访问并添加错误提示。当全局状态超过3-4个时,建议切换到Zustand。
Zustand:Expo项目首选方案
Zustand是当前Expo社区推荐度最高的状态管理库。2025年React状态管理调查中,Zustand的"保留率"和"兴趣度"均排名第一。它体积小、API简洁、无需Provider包裹、原生支持React Native。
基础用法
typescriptimport { create } from 'zustand'; interface CartItem { id: string; name: string; price: number; quantity: number; } interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: string) => void; totalPrice: () => number; } const useCartStore = create<CartStore>((set, get) => ({ items: [], addItem: (item) => set((state) => { const existing = state.items.find((i) => i.id === item.id); if (existing) { return { items: state.items.map((i) => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ), }; } return { items: [...state.items, item] }; }), removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id), })), totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0), }));
在组件中使用
typescriptfunction CartScreen() { // 用selector精确订阅,避免不必要的重渲染 const items = useCartStore((s) => s.items); const addItem = useCartStore((s) => s.addItem); const totalPrice = useCartStore((s) => s.totalPrice); return ( <View> {items.map((item) => ( <Text key={item.id}>{item.name} x{item.quantity}</Text> ))} <Text>总计: {totalPrice()}</Text> </View> ); }
持久化状态
Expo应用中经常需要将用户偏好、登录状态等持久化到本地。Zustand提供了persist中间件,配合expo-secure-store使用:
typescriptimport { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import * as SecureStore from 'expo-secure-store'; // 封装SecureStore适配器 const secureStorage = { getItem: async (name: string) => { return await SecureStore.getItemAsync(name); }, setItem: async (name: string, value: string) => { await SecureStore.setItemAsync(name, value); }, removeItem: async (name: string) => { await SecureStore.deleteItemAsync(name); }, }; interface SettingsStore { theme: 'light' | 'dark'; locale: string; setTheme: (theme: 'light' | 'dark') => void; setLocale: (locale: string) => void; } const useSettingsStore = create<SettingsStore>()( persist( (set) => ({ theme: 'light', locale: 'zh', setTheme: (theme) => set({ theme }), setLocale: (locale) => set({ locale }), }), { name: 'app-settings', storage: createJSONStorage(() => secureStorage), } ) );
调试技巧
Zustand可以通过devtools中间件连接Redux DevTools。在Expo开发构建中,有社区插件可以直接在Expo Go中调试Zustand store。
Redux Toolkit:大型团队的结构化选择
Redux Toolkit适合10人以上团队、50+个store模块的大型应用。它的强项在于严格的代码规范和强大的调试工具,但样板代码多、学习成本高。
Store配置与Slice
typescriptimport { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'; import { Provider, useSelector, useDispatch } from 'react-redux'; // 异步action:处理API请求 export const fetchProducts = createAsyncThunk( 'products/fetch', async (category: string) => { const response = await fetch(`/api/products?category=${category}`); return response.json(); } ); const productsSlice = createSlice({ name: 'products', initialState: { items: [] as Product[], loading: false, error: string | null, }, reducers: { clearProducts: (state) => { state.items = []; }, }, extraReducers: (builder) => { builder .addCase(fetchProducts.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchProducts.fulfilled, (state, action) => { state.loading = false; state.items = action.payload; }) .addCase(fetchProducts.rejected, (state, action) => { state.loading = false; state.error = action.error.message ?? 'Unknown error'; }); }, }); // 在Expo应用根组件包裹Provider const store = configureStore({ reducer: { products: productsSlice.reducer, }, }); function App() { return ( <Provider store={store}> <RootLayout /> </Provider> ); }
Redux Toolkit的优势在于团队协作:严格的单向数据流和action日志让多人协作时状态变更可追溯。如果你的团队已经在用Redux且没有明显痛点,不必迁移。
Jotai:细粒度原子化状态
Jotai的原子化模型适合组件间有复杂依赖关系的场景,比如表单构建器、数据看板等。每个atom独立存在,只有订阅了该atom的组件才会在其变化时重新渲染。
typescriptimport { atom, useAtom } from 'jotai'; // 基础atom const filterAtom = atom('all'); const searchQueryAtom = atom(''); // 派生atom:依赖其他atom,自动缓存计算结果 const filteredItemsAtom = atom((get) => { const filter = get(filterAtom); const query = get(searchQueryAtom); return allItems.filter((item) => { const matchFilter = filter === 'all' || item.category === filter; const matchQuery = item.name.toLowerCase().includes(query.toLowerCase()); return matchFilter && matchQuery; }); }); // 可写派生atom:同时读写 const toggleFilterAtom = atom( (get) => get(filterAtom), (get, set, newFilter: string) => { set(filterAtom, newFilter); set(searchQueryAtom, ''); // 切换分类时清空搜索 } ); function FilterBar() { const [filter, setFilter] = useAtom(toggleFilterAtom); const [query, setQuery] = useAtom(searchQueryAtom); return ( <View> <TextInput value={query} onChangeText={setQuery} placeholder="搜索..." /> <Picker selectedValue={filter} onValueChange={setFilter}> <Picker.Item label="全部" value="all" /> <Picker.Item label="电子产品" value="electronics" /> </Picker> </View> ); } function ItemList() { const [items] = useAtom(filteredItemsAtom); return ( <FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => <Text>{item.name}</Text>} /> ); }
Jotai的派生atom机制让状态依赖关系清晰可读,但atom数量多时管理成本上升,适合状态依赖复杂但总量可控的场景。
服务端状态:别忘了TanStack Query
以上方案管理的是客户端状态。对于API请求、缓存、后台刷新等服务端状态,应该使用TanStack Query(React Query)。它和任何客户端状态管理库可以并存:
typescriptimport { useQuery } from '@tanstack/react-query'; function ProductList({ category }: { category: string }) { const { data, isLoading, error, refetch } = useQuery({ queryKey: ['products', category], queryFn: () => fetchProducts(category), staleTime: 5 * 60 * 1000, // 5分钟内不重新请求 }); if (isLoading) return <ActivityIndicator />; if (error) return <Text>加载失败</Text>; return ( <FlatList data={data} keyExtractor={(item) => item.id} renderItem={({ item }) => <ProductCard product={item} />} refreshing={false} onRefresh={refetch} /> ); }
将服务端状态和客户端状态分离是Expo应用架构的重要原则:TanStack Query管API数据,Zustand或Jotai管UI状态。
实战选型建议
1-5个页面的个人项目:Context API足够,不用引入额外依赖。
5-20个页面的中型项目:Zustand。API简单,3KB体积对移动端友好,持久化中间件开箱即用。
20+页面、多人协作的大型项目:Redux Toolkit。结构化规范减少沟通成本,DevTools让问题排查效率翻倍。
状态依赖复杂的表单/看板类应用:Jotai。派生atom让计算逻辑内聚,避免props层层传递。
有大量API交互的项目:Zustand + TanStack Query组合。Zustand管UI和导航状态,TanStack Query管服务端缓存和同步。
无论选择哪个方案,注意三条原则:优先使用局部状态而非全局状态;用selector精确订阅,避免整棵状态树触发重渲染;移动端对包大小敏感,能不引入的依赖就不引入。