You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redux-Toolkit Query首次请求报错,后续请求成功却无数据展示

问题描述

实现测试结果页面分页功能时遇到以下异常:

  • 首次进入TestResultsList页面,useGetTestResultsQuery发起的请求参数仅为'testResultsList',后端返回400错误(提示“Username not found”),请求result为undefined
  • 随后自动重新请求,控制台显示result包含有效数据(ids数组、entities对象、totalPages:2、currentUser对象),参数也完整(['testResultsList', {...}]),但页面无数据展示
  • Redux DevTools中testResultsList始终显示首次请求的“Username not found”错误,无有效数据
  • 切换分页或触发refetchOnFocus时,上述流程重复:控制台显示有效数据,但错误状态持续

相关代码

TestResultsList.jsx

const TestResultsList = () => {
  const { username } = useAuth();

  const [selectedTestResultId, setSelectedTestResultId] = useState(null);
  const [isEditing, setIsEditing] = useState(false);
  const [search, setSearch] = useState('');
  const [currentPage, setCurrentPage] = useState(1);
  const itemsPerPage = 10;

  const {
    data: testResults,
    isLoading,
    isSuccess,
    isError,
    error,
  } = useGetTestResultsQuery(
    ['testResultsList', { page: currentPage, limit: itemsPerPage, username }],
    {
      pollingInterval: 60000,
      refetchOnFocus: true,
      refetchOnMountOrArgChange: true,
    }
  );

testResultsApiSlice.js

import { createEntityAdapter, createSelector } from '@reduxjs/toolkit';
import { apiSlice } from '../../app/api/apiSlice';

const testResultsAdapter = createEntityAdapter({});

const initialState = testResultsAdapter.getInitialState();

export const testResultsApiSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    getTestResults: builder.query({
      query: ([, { page, limit, username }]) => ({
        url: `/testResults`,
        params: { page, limit, username },
        validateStatus: (response, result) => {
          return response.status === 200 && !result.isError;
        },
      }),
      transformResponse: (responseData) => {
        const loadedTestResults = responseData.testResults.map((testResult) => {
          testResult.id = testResult._id;
          return testResult;
        });
        return {
          ...testResultsAdapter.setAll(initialState, loadedTestResults),
          totalPages: responseData.totalPages,
          currentUser: responseData.currentUser,
        };
      },
      providesTags: (result, error, arg) => {
        console.log(result);
        console.log(arg);

        if (result?.ids) {
          return [
            { type: 'TestResult', id: 'LIST' },
            ...result.ids.map((id) => ({ type: 'TestResult', id })),
          ];
        } else return [{ type: 'TestResult', id: 'LIST' }];
      },
    }),
    addNewTestResult: builder.mutation({
      query: (formData) => ({
        url: '/testResults',
        method: 'POST',
        body: formData,
      }),
      invalidatesTags: [{ type: 'TestResult', id: 'LIST' }],
    }),
    updateTestResult: builder.mutation({
      query: (formData) => ({
        url: '/testResults',
        method: 'PATCH',
        body: formData,
      }),
      invalidatesTags: (result, error, arg) => [
        { type: 'TestResult', id: arg.id },
      ],
    }),
    deleteTestResult: builder.mutation({
      query: ({ id }) => ({
        url: '/testResults',
        method: 'DELETE',
        body: { id },
      }),
      invalidatesTags: (result, error, arg) => [
        { type: 'TestResult', id: arg.id },
      ],
    }),
  }),
});

export const {
  useGetTestResultsQuery,
  useAddNewTestResultMutation,
  useUpdateTestResultMutation,
  useDeleteTestResultMutation,
} = testResultsApiSlice;

export const selectTestResultsResult =
  testResultsApiSlice.endpoints.getTestResults.select();

const selectTestResultsData = createSelector(
  selectTestResultsResult,
  (testResultsResult) => testResultsResult.data
);

export const {
  selectAll: selectAllTestResults,
  selectById: selectTestResultById,
  selectIds: selectTestResultIds,
} = testResultsAdapter.getSelectors(
  (state) => selectTestResultsData(state) ?? initialState
);

后端getAllTestResults控制器代码

const getAllTestResults = async (req,res) => {
  const { page, limit, username } = req.query;
  const skip = (page - 1) * limit;

  const currentUser = await User.findOne({ username })
    .select('-password')
    .lean()
    .exec();

  if (!currentUser) {
    return res.status(400).json({ message: 'Current user not found' });
  }

  const totalTestResults = await TestResult.countDocuments({ user: currentUser._id });
  const totalPages = Math.ceil(totalTestResults / limit);

  const testResults = await TestResult.find({ user: currentUser._id })
    .skip(skip)
    .limit(limit)
    .lean();

  res.status(200).json({
    totalPages,
    testResults,
    currentUser
  });
}

问题分析与解决方案

核心问题1:首次请求参数缺失username

首次渲染时useAuth()返回的username可能未加载完成,导致请求携带undefined的username,甚至触发RTK Query参数解析异常,出现仅'testResultsList'的无效参数。

解决方法:
添加skip配置,确保username存在时才发起请求:

const {
  data: testResults,
  isLoading,
  isSuccess,
  isError,
  error,
} = useGetTestResultsQuery(
  ['testResultsList', { page: currentPage, limit: itemsPerPage, username }],
  {
    pollingInterval: 60000,
    refetchOnFocus: true,
    refetchOnMountOrArgChange: true,
    skip: !username, // 无username时跳过请求
  }
);

核心问题2:错误状态未被正确覆盖

首次请求的错误被缓存后,后续成功请求的结果无法覆盖错误状态,导致组件始终读取错误缓存。

解决方法:

  1. 调整validateStatus逻辑,避免错误的状态判断:
validateStatus: (response) => {
  // 仅将200视为成功响应
  return response.status === 200;
},
  1. 优化refetchOnMountOrArgChange配置,确保参数变化时强制更新缓存:
{
  pollingInterval: 60000,
  refetchOnFocus: true,
  refetchOnMountOrArgChange: {
    refetchOnMount: true,
    refetchOnArgChange: true,
    refetchOnReconnect: true,
  },
  skip: !username,
}

核心问题3:缓存键不统一

首次无效请求的缓存键(带undefined的username)与后续有效请求的缓存键不一致,导致组件读取错误的缓存条目。

解决方法:
通过skip配置避免发起无效请求,确保所有请求都携带有效username,让缓存键统一,后续成功请求会自动覆盖之前的错误缓存。


内容的提问来源于stack exchange,提问作者zxc123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 19:47:05