React Query处理关联数据:是否有现成库及缓存解决方案?
一、React Query原生方案:精细化queryKey+手动缓存合并
React Query本身没有内置实体级的关联数据管理,但可以通过区分queryKey和手动合并缓存解决问题:
用包含关联字段的queryKey区分不同请求:
给不同页面的请求添加关联参数到queryKey中,比如页面X的queryKey设为['person', personId, { include: ['addresses'] }],页面Y设为['person', personId, { include: ['cards'] }],这样两个请求会生成独立的缓存条目,不会互相覆盖。手动合并缓存确保数据一致性:
如果希望同一个Person的缓存能整合不同请求回来的关联数据,可以在请求完成后,用queryClient.setQueryData将新数据合并到基础Person缓存中:const fetchPerson = async (personId, include) => { const res = await fetch(`/api/persons/${personId}?include=${include.join(',')}`); const data = await res.json(); // 合并到基础Person缓存 const existingPerson = queryClient.getQueryData(['person', personId]) || {}; queryClient.setQueryData(['person', personId], { ...existingPerson, ...data, // 合并关联数组,避免覆盖已有数据 addresses: [...(existingPerson.addresses || []), ...(data.addresses || [])], cards: [...(existingPerson.cards || []), ...(data.cards || [])], }); return data; }; // 页面X使用 const { data: personWithAddresses } = useQuery({ queryKey: ['person', personId, { include: ['addresses'] }], queryFn: () => fetchPerson(personId, ['addresses']), }); // 页面Y使用 const { data: personWithCards } = useQuery({ queryKey: ['person', personId, { include: ['cards'] }], queryFn: () => fetchPerson(personId, ['cards']), });这样基础缓存
['person', personId]会始终包含所有已请求过的关联数据,不同页面的请求也不会互相干扰。
二、社区库:@data-client/react(原Rest Hooks)
如果想要类似Ember那样自动实体关联、缓存规范化的开箱即用体验,推荐使用@data-client/react,它专门针对REST API的关联数据管理设计,内置了实体模型和缓存合并逻辑:
定义实体模型:
import { Entity, Resource } from '@data-client/rest'; class Address extends Entity { id = ''; street = ''; city = ''; // 定义实体主键 pk() { return this.id; } } class Card extends Entity { id = ''; number = ''; expiry = ''; pk() { return this.id; } } class Person extends Entity { id = ''; age = 0; name = ''; addresses: Address[] = []; cards: Card[] = []; pk() { return this.id; } // 声明关联关系 static schema = { addresses: [Address], cards: [Card], }; }创建资源并发起请求:
const PersonResource = Resource.fromEntity(Person); // 页面X请求带addresses的Person const personWithAddresses = useSuspense(PersonResource.get, { id: personId, params: { include: 'addresses' }, }); // 页面Y请求带cards的Person const personWithCards = useSuspense(PersonResource.get, { id: personId, params: { include: 'cards' }, });这个库会自动按实体id缓存Person、Address、Card,不同请求返回的关联数据会自动合并到对应的实体缓存中,完全不需要手动处理,和Ember的模型更新逻辑一致。
三、自定义缓存规范化(无依赖方案)
如果不想引入第三方库,可以自己实现实体级缓存规范化:
- 将每个实体(Person、Address、Card)按id单独存储到缓存中
- 当请求Person时,从基础缓存中取出Person数据,再关联对应的Address/Card缓存组合成完整数据
示例代码:
// 存储单个实体的工具函数 const setEntity = (entityType, entity) => { queryClient.setQueryData([entityType, entity.id], entity); }; // 获取单个实体的工具函数 const getEntity = (entityType, id) => { return queryClient.getQueryData([entityType, id]); }; // 请求并规范化Person数据 const fetchPerson = async (personId, include) => { const res = await fetch(`/api/persons/${personId}?include=${include.join(',')}`); const data = await res.json(); // 存储Person实体 setEntity('person', data); // 存储关联的Address和Card实体 data.addresses?.forEach(addr => setEntity('address', addr)); data.cards?.forEach(card => setEntity('card', card)); return data; }; // 获取带关联数据的Person const getPersonWithRelations = (personId) => { const person = getEntity('person', personId); if (!person) return null; return { ...person, addresses: person.addresses?.map(addr => getEntity('address', addr.id)), cards: person.cards?.map(card => getEntity('card', card.id)), }; }; // 使用时 const { data: rawPerson } = useQuery({ queryKey: ['person', personId, { include: ['addresses'] }], queryFn: () => fetchPerson(personId, ['addresses']), }); const personWithAddresses = getPersonWithRelations(personId);
这种方式完全基于React Query的原生API实现,能实现类似Ember的实体关联更新效果。
内容的提问来源于stack exchange,提问作者Isonlaxman

