React页面切换前组件重渲染引发无效API调用问题修复求助
我有一个React应用,可在automatic和manual两个页面间切换,两个页面展示不同数据。流程为:先从服务器获取对应页面的批次列表,再生成表格,且为每个批次再次调用服务器获取批次数据。例如调用"/api/manual/list_batches"获取manual批次列表,再用批次名调用"/api/manual/get_batch?batch_name=foo_manual_batch"获取单批次数据。
切换到automatic页面时,预期会清空旧表格并调用"/api/automatic/list_batches"获取新批次列表,再为新批次调用对应API,但实际在获取新批次列表前,表格检测到URL中的mode从manual变为automatic,会尝试用旧批次名调用"/api/automatic/get_batch?batch_name=foo_manual_batch",导致错误。页面功能正常,但存在不必要的无效API调用。
复现代码
App组件
import React from "react"; import { Route, Switch } from "react-router-dom"; import BatchesPage from "./BatchesPage"; import Navigation from "./Navigation"; import "./styles.css"; export default function App() { return ( <div> <Switch> <Route exact path="/"> <Navigation /> </Route> <Route exact path="/:mode"> <Navigation /> <BatchesPage /> </Route> </Switch> </div> ); }
BatchesPage组件
import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import BatchesTable from "./BatchesTable"; import { fetchJson } from "./model"; export default function BatchesPage() { const [batchesNames, setBatchesNames] = useState([]); const { mode } = useParams(); const getBatchesNamesCmd = `/api/${mode}/list_batches`; // 根据mode加载automatic或manual批次名称 useEffect(() => { fetchJson(getBatchesNamesCmd) .then((jsonData) => { setBatchesNames(jsonData.sort().reverse()); }) .catch((error) => { console.error(`error occure: ${error}`); }); }, [getBatchesNamesCmd]); return batchesNames ? ( <div> {mode === "automatic" ? ( <div className="horizontal-container"> {["fruit", "vegetable"].map((keyword) => ( <BatchesTable key={keyword} batchesNames={batchesNames.filter((name) => name.includes(keyword) )} viewMode="narrow" tableName={keyword} /> ))} </div> ) : ( <BatchesTable batchesNames={batchesNames} viewMode="wide" /> )} </div> ) : ( <div></div> ); }
核心问题是mode切换后,旧的batchesNames未被清空就传给BatchesTable,导致表格用旧批次名+新mode发起无效请求。以下是几种可行的修复方案:
方案一:添加加载状态,仅在新数据就绪后渲染表格
修改BatchesPage组件,新增加载状态,在mode切换时立即清空旧数据,直到新批次列表获取完成再渲染表格:
import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import BatchesTable from "./BatchesTable"; import { fetchJson } from "./model"; export default function BatchesPage() { const [batchesNames, setBatchesNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const { mode } = useParams(); const getBatchesNamesCmd = `/api/${mode}/list_batches`; useEffect(() => { // mode切换时立即重置状态 setIsLoading(true); setBatchesNames([]); fetchJson(getBatchesNamesCmd) .then((jsonData) => { setBatchesNames(jsonData.sort().reverse()); }) .catch((error) => { console.error(`error occure: ${error}`); }) .finally(() => { setIsLoading(false); }); }, [getBatchesNamesCmd]); // 加载中时不渲染表格,避免旧数据触发无效请求 if (isLoading) { return <div>加载中...</div>; } return ( <div> {mode === "automatic" ? ( <div className="horizontal-container"> {["fruit", "vegetable"].map((keyword) => ( <BatchesTable key={keyword} batchesNames={batchesNames.filter((name) => name.includes(keyword) )} viewMode="narrow" tableName={keyword} /> ))} </div> ) : ( <BatchesTable batchesNames={batchesNames} viewMode="wide" /> )} </div> ); }
方案二:在表格组件中绑定mode依赖,过滤无效批次
如果不想修改页面加载状态,可以在BatchesTable组件中接收mode参数,监听mode变化并清空自身数据,确保仅处理当前mode对应的批次:
修改BatchesTable组件
import React, { useEffect, useState } from "react"; export default function BatchesTable({ batchesNames, mode, ...rest }) { const [currentBatches, setCurrentBatches] = useState([]); useEffect(() => { // mode变化时清空当前批次,等待新的有效数据 setCurrentBatches([]); setCurrentBatches(batchesNames); }, [mode, batchesNames]); // 无有效批次时不渲染表格内容 if (!currentBatches.length) { return <div></div>; } // 原表格渲染逻辑... }
在BatchesPage中传递mode参数
// automatic分支 <BatchesTable key={keyword} mode={mode} batchesNames={batchesNames.filter((name) => name.includes(keyword))} viewMode="narrow" tableName={keyword} /> // manual分支 <BatchesTable mode={mode} batchesNames={batchesNames} viewMode="wide" />
方案三:使用AbortController取消旧请求(可选)
如果mode切换时旧的list_batches请求仍在进行,可以用AbortController取消它,避免无效的状态更新:
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; setIsLoading(true); setBatchesNames([]); fetchJson(getBatchesNamesCmd, { signal }) .then((jsonData) => { setBatchesNames(jsonData.sort().reverse()); }) .catch((error) => { // 忽略请求取消的错误 if (error.name !== 'AbortError') { console.error(`error occure: ${error}`); } }) .finally(() => { setIsLoading(false); }); // 组件卸载或依赖变化时取消请求 return () => controller.abort(); }, [getBatchesNamesCmd]);
需要同步修改fetchJson函数支持signal参数:
export async function fetchJson(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); }
内容的提问来源于stack exchange,提问作者dannymo

