如何在RTK Query中实现跨查询的实体数据关联?
RTK Query 关联实体的集中序列化方案
问题背景
现有实体接口:
interface MyEntity { // 基础属性 id: number, // ... authorIds: number[], authors: User[], }
后端API返回结果仅包含ID数组,不提供完整User对象:
{ "id": 1, "authorIds": [1, 2, 3] }
已配置getMyEntities和getUsers两个RTK Query查询,但目前需在React组件中手动映射authorIds到User对象,存在冗余,希望在状态层集中处理该关联逻辑,询问是否可通过transformResponse或中间件实现(已知transformResponse无法访问状态/查询缓存)。
解决方案
方案一:利用自定义Selector组合缓存数据
RTK Query会自动缓存查询结果,可通过Redux Toolkit的createSelector组合两个查询的缓存,实现实体关联:
- 先创建用户实体的映射Selector,将用户数组转为ID键的对象:
import { createSelector } from '@reduxjs/toolkit'; import { api } from './yourApiSlice'; // 获取getUsers的缓存结果 const selectUsersResult = api.endpoints.getUsers.select(); // 转换为{ [userId]: User }的映射 const selectUsersById = createSelector( selectUsersResult, (usersResult) => { if (!usersResult.data) return {}; return usersResult.data.reduce((map, user) => { map[user.id] = user; return map; }, {} as Record<number, User>); } );
- 创建组合后的
MyEntitySelector,自动填充authors字段:
export const selectMyEntitiesWithAuthors = createSelector( api.endpoints.getMyEntities.select(), selectUsersById, (entitiesResult, usersById) => { if (!entitiesResult.data) return []; return entitiesResult.data.map(entity => ({ ...entity, authors: entity.authorIds.map(id => usersById[id]).filter(Boolean) })); } );
- 组件中使用:
// 先触发用户查询(确保缓存有数据) const { data: users } = useGetUsersQuery(); // 直接获取已关联好的MyEntity数据 const myEntities = useSelector(selectMyEntitiesWithAuthors);
方案二:通过onQueryStarted钩子自动关联
在getMyEntities的查询配置中,利用onQueryStarted钩子,在获取到实体数据后自动触发用户查询,并更新缓存中的MyEntity:
const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/' }), endpoints: (builder) => ({ getMyEntities: builder.query<MyEntity[], void>({ query: () => '/myEntities', async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) { // 等待MyEntity数据获取完成 const { data: entities } = await queryFulfilled; // 收集所有需要的作者ID const allAuthorIds = entities.flatMap(e => e.authorIds); // 触发getUsers查询(若未加载则自动请求) await dispatch(api.endpoints.getUsers.initiate(allAuthorIds)); // 从缓存中获取用户数据并转为ID映射 const usersResult = api.endpoints.getUsers.select(allAuthorIds)(getState()); const usersById = usersResult.data?.reduce((map, u) => ({...map, [u.id]: u}), {}) || {}; // 更新MyEntity的缓存数据,填充authors字段 dispatch( api.util.updateQueryData('getMyEntities', arg, (draft) => { draft.forEach(entity => { entity.authors = entity.authorIds.map(id => usersById[id]).filter(Boolean); }); }) ); }, }), getUsers: builder.query<User[], number[]>({ query: (ids) => `/users?ids=${ids.join(',')}`, }), }), });
之后使用useGetMyEntitiesQuery时,返回的data会自动包含填充好的authors字段。
方案三:自定义Redux中间件实现通用关联
如果需要处理多种实体的关联逻辑,可以编写自定义中间件,监听RTK Query的成功Action,自动触发关联查询并更新实体:
const entityAssociationMiddleware = store => next => action => { // 监听getMyEntities查询完成的Action if (api.endpoints.getMyEntities.matchFulfilled(action)) { const entities = action.payload; const allAuthorIds = entities.flatMap(e => e.authorIds); // 触发用户查询 store.dispatch(api.endpoints.getUsers.initiate(allAuthorIds)); // 订阅状态变化,等待用户数据加载完成后更新实体 const unsubscribe = store.subscribe(() => { const usersResult = api.endpoints.getUsers.select(allAuthorIds)(store.getState()); if (usersResult.data) { const usersById = usersResult.data.reduce((map, u) => ({...map, [u.id]: u}), {}); // 更新MyEntity缓存 store.dispatch( api.util.updateQueryData('getMyEntities', undefined, (draft) => { draft.forEach(entity => { entity.authors = entity.authorIds.map(id => usersById[id]).filter(Boolean); }); }) ); unsubscribe(); } }); } return next(action); };
在configureStore时将该中间件加入即可:
export const store = configureStore({ reducer: { [api.reducerPath]: api.reducer, // 其他reducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware, entityAssociationMiddleware), });
关键说明
transformResponse为同步纯函数,仅负责API返回数据的格式转换,无法访问Redux状态或其他查询缓存,不适合跨实体关联场景。- 优先推荐方案一或方案二,更贴合RTK Query的设计理念;方案三适合需要处理多种实体关联的通用场景。
内容的提问来源于stack exchange,提问作者Lucas Minatal
相关产品推荐
相关产品推荐

