React Native运动追踪项目中Apollo Client与Zustand结合的价值疑问
Apollo Client 与 Zustand 搭配的价值及优化建议
核心结论
Apollo Client 和 Zustand 并非重复工具,而是各司其职的状态管理方案:Apollo 专注服务端同步数据的缓存与请求管理,Zustand 负责客户端本地生成的状态。你的当前代码存在不必要的重复存储,但两者结合在特定场景下有明确价值。
一、两者的核心定位差异
- Apollo Client:
- 管理从服务端拉取的、需与后端同步的数据,自带全局缓存、请求去重、缓存自动更新(如 mutations 后自动关联查询更新)、分页/加载状态管理等能力。
- 全局可访问,任意组件通过
useQuery即可直接获取缓存数据,无需手动同步。
- Zustand:
- 管理客户端本地生成的、无需和后端同步的状态,比如:
- UI 状态(侧边栏展开/收起、选中的标签页)
- 临时表单数据、本地筛选/排序后的衍生数据
- 用户本地偏好(主题模式、默认视图)
- 管理客户端本地生成的、无需和后端同步的状态,比如:
二、你当前代码的问题:不必要的重复存储
你将 Apollo 获取的 data 存入 Zustand,属于重复存储服务端数据——因为 Apollo 的缓存已经是全局可访问的,直接在其他组件用 useQuery(GET_EXERCICES) 就能拿到最新数据,完全不需要额外存到 Zustand 里,这只会增加维护成本(比如需要手动同步缓存更新)。
三、两者结合的合理场景
只有当你需要对服务端数据做本地加工,或结合客户端状态时,才需要搭配使用:
- 衍生本地状态:
比如从 Apollo 拿到原始运动列表后,需要做本地筛选、排序、分组,这些计算后的结果可以存在 Zustand 里,避免每个组件重复计算。 - 客户端临时状态与服务端数据联动:
比如用户临时收藏某个运动(还没同步到后端),收藏状态存在 Zustand,和 Apollo 返回的运动列表结合展示“已收藏”标记。 - 复杂跨组件状态联动:
多个组件需要共同修改某个和服务端数据相关的本地状态时,Zustand 的全局状态比组件传 props 更高效。
四、优化后的代码示例
场景1:直接使用 Apollo 缓存(无本地加工需求)
去掉 Zustand 的存储逻辑,直接用 Apollo 的数据:
const Exercices = () => { const { token } = useAuthStore(); const { data: exercices, loading, error } = useQuery(GET_EXERCICES, { context: { headers: { Authorization: `Bearer ${token}`, }, }, }); if (loading) { return <Text>Loading...</Text>; } if (error) { return <Text>Error</Text>; } // 直接使用 exercices 渲染 return ( <View> {exercices?.map(exercise => ( <Text key={exercise.id}>{exercise.name}</Text> ))} </View> ); };
场景2:结合 Zustand 管理本地筛选状态
如果需要本地筛选运动列表,Zustand 负责存储筛选条件和衍生结果:
// Zustand store const useExerciceStore = create((set, get) => ({ filterText: '', setFilterText: (text) => set({ filterText: text }), getFilteredExercices: (rawExercices) => { const { filterText } = get(); return rawExercices?.filter(ex => ex.name.toLowerCase().includes(filterText.toLowerCase()) ) || []; }, })); // 组件中使用 const Exercices = () => { const { token } = useAuthStore(); const { filterText, setFilterText, getFilteredExercices } = useExerciceStore(); const { data: rawExercices, loading, error } = useQuery(GET_EXERCICES, { context: { headers: { Authorization: `Bearer ${token}`, }, }, }); const filteredExercices = getFilteredExercices(rawExercices); if (loading) { return <Text>Loading...</Text>; } if (error) { return <Text>Error</Text>; } return ( <View> <TextInput value={filterText} onChangeText={setFilterText} placeholder="筛选运动..." /> {filteredExercices.map(exercise => ( <Text key={exercise.id}>{exercise.name}</Text> ))} </View> ); };
内容的提问来源于stack exchange,提问作者Jeff
相关产品推荐
相关产品推荐

