RTK Query选择器返回undefined但状态存在数据的问题排查
问题:Redux Toolkit Query选择器无法获取预约数据
通过GET请求http://localhost:5000/shops/id/appointments获取指定店铺预约信息,Redux DevTools显示请求成功且状态中存在数据,但组件使用selectAppointmentsById选择器始终返回undefined。
前端请求实现
在AppointmentsList组件中通过路由参数id调用查询钩子:
const AppointmentsList = ()=>{ const {id} = useParams(); const { data:appointments, isLoading, isSuccess, isError, error } = useGetAppointmentsQuery(id);
后端响应数据
后端返回的JSON格式预约数据:
[ { "active": true, "customerName": "Takis Makis", "service": "Kourema", "date": "12/12/2022", "_id": "63923aac73a0d02225738e69" }, { "active": true, "customerName": "Mixalis Mixaliotis", "service": "Lousimo", "date": "01/-1/2023", "_id": "63923ac973a0d02225738e6d" }, { "active": true, "customerName": "Ilias Tontoktoris", "service": "Xirisma", "date": "10/1/2023", "_id": "63923ae173a0d02225738e72" } ]
相关代码实现
appointmentsSlice代码
import { createSelector,createEntityAdapter } from "@reduxjs/toolkit"; import { apiSlice } from "../../app/api/apiSlice"; const appointmentsAdapter = createEntityAdapter({}); const initialState = appointmentsAdapter.getInitialState(); export const appointmentsApiSlice = apiSlice.injectEndpoints({ endpoints:builder=>({ getAppointments:builder.query({ query:(id)=>`/shops/${id}/appointments`, validateStatus:(response,result)=>{ return response.status === 200 && !result.isError }, keepUnusedDataFor:60, transformResponse:responseData=>{ const loadedAppointments = responseData.map(appointment =>{ appointment.id = appointment._id return appointment }); return appointmentsAdapter.setAll(initialState,loadedAppointments) }, providesTags:(result,error,arg)=>{ if(result?.ids) { return [ { type:'Appointment', id:'LIST' }, ...result.ids.map(id=>({type:'Appointment',id})) ] } else return [{type:'Appointment',id:'LIST'}] } }),
apiSlice代码
import {createApi,fetchBaseQuery} from "@reduxjs/toolkit/query/react"; export const apiSlice = createApi({ baseQuery:fetchBaseQuery({baseUrl:'http://localhost:5000'}), tagTypes:['Shop','User','Appointment'], endpoints:builder =>({}) });
Store配置
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; import {apiSlice} from './api/apiSlice'; import {setupListeners} from '@reduxjs/toolkit/dist/query'; export const store = configureStore({ reducer:{ [apiSlice.reducerPath]:apiSlice.reducer, }, middleware:getDefaultMiddleware=> getDefaultMiddleware().concat(apiSlice.middleware), devTools:true }); setupListeners(store.dispatch);
前端路由配置
<Routes> <Route path='/' element={<Layout/>}> <Route index element={<Public/>}/> <Route path='/login' element={<Login/>}/> <Route path='/shops/:id/appointment'></Route> {/* 公开路由,用于创建预约 */} <Route path='dash' element={<DashLayout/>}> <Route index element={<Welcome/>}/> <Route path='shops'> <Route index element={<ShopsList/>}/> {/* 管理员可查看所有店铺,店主仅能查看自己的店铺*/} <Route path=':id'element={<AppointmentsList/>}/> </Route> </Route> </Route> </Routes>
Appointment组件代码
const Appointment = ({appId})=>{ const appointment = useSelector(state=>selectAppointmentsById(state,appId)); console.log("test:",appointment) const navigate= useNavigate(); if(appointment){ const customerName = appointment.customerName; const service = appointment.service; const date = appointment.date; const handleEdit =()=>navigate(`/dash/appointments/edit/${appointment._id}`); return( <tr> <td>{customerName}</td> <td>{service}</td> <td>{date}</td> <td><button onClick={handleEdit}>Επεξεργασία</button></td> </tr> ) } else return null } export default Appointment;
完整appointmentsSlice补充代码
const appointmentsAdapter = createEntityAdapter({}); const initialState = appointmentsAdapter.getInitialState(); export const appointmentsApiSlice = apiSlice.injectEndpoints({ endpoints:builder=>({ getAppointments:builder.query({ query:(id)=>`/shops/${id}/appointments`, validateStatus:(response,result)=>{ return response.status === 200 && !result.isError }, keepUnusedDataFor:60, transformResponse:responseData=>{ const loadedAppointments = responseData.map(appointment =>{ appointment.id = appointment._id return appointment }); return appointmentsAdapter.setAll(initialState,loadedAppointments) }, providesTags:(result,error,arg)=>{ if(result?.ids) { return [ { type:'Appointment', id:'LIST' }, ...result.ids.map(id=>({type:'Appointment',id})) ] } else return [{type:'Appointment',id:'LIST'}] } }), export const { useGetAppointmentsQuery, } = appointmentsApiSlice export const selectAppointmentsResult = appointmentsApiSlice.endpoints.getAppointments.select(); const selectAppointmentsData = createSelector( selectAppointmentsResult, appointmentsResult=>appointmentsResult.data ) export const { selectAll:selectAllAppointments, selectById:selectAppointmentsById, selectIds:selectAppointmentsIds, } = appointmentsAdapter.getSelectors(state=>selectAppointmentsData(state)??initialState)
问题分析与解决方案
核心问题
RTK Query会根据查询参数生成唯一缓存键,当前selectAppointmentsResult未传入店铺id参数,导致selector尝试获取无参查询的缓存,无法匹配到对应店铺的预约数据。
修复方案
方案一:修改Selector支持参数传递
- 更新
selectAppointmentsResult,使其接受查询参数:
export const selectAppointmentsResult = (shopId) => appointmentsApiSlice.endpoints.getAppointments.select(shopId);
- 在
Appointment组件中,先获取对应店铺的查询结果,再提取单个预约:
import { useParams } from 'react-router-dom'; import { selectAppointmentsResult, selectAppointmentsById } from './appointmentsSlice'; const Appointment = ({appId})=>{ const {id: shopId} = useParams(); const appointmentsResult = useSelector(selectAppointmentsResult(shopId)); const appointment = selectAppointmentsById(appointmentsResult.data ?? initialState, appId); console.log("test:", appointment); // 其余代码不变 }
方案二:直接传递数据给子组件
在AppointmentsList组件中直接将预约数据传递给Appointment,避免子组件使用selector查询:
// AppointmentsList组件中 if(isSuccess){ return ( <table> <tbody> {selectAppointmentsIds(appointments).map(appId => ( <Appointment key={appId} appointment={selectAppointmentsById(appointments, appId)} /> ))} </tbody> </table> ) } // Appointment组件直接使用props数据 const Appointment = ({appointment})=>{ if(!appointment) return null; // 直接使用appointment数据渲染 }
内容的提问来源于stack exchange,提问作者Kwstas Greco
相关产品推荐
相关产品推荐

