React调用星球大战API时useEffect效率优化与状态设置问题
问题根源
当前代码存在几个核心错误导致数据提前渲染:
- 调用
fetch时没有返回Promise实例,Promise.all实际接收到的是一堆undefined,根本没有等待网络请求完成就执行了后续的赋值操作 - 缺少SWAPI分页拉取逻辑,目前仅能拉取第一页的10个角色数据
- 直接修改接口返回的原始对象,违反React不可变数据原则,容易触发意外渲染问题
- 没有等待全量角色的所有关联数据拉取完成,就提前更新了
characters状态
修正方案
完整逻辑代码
const api = `https://swapi.dev/api/people/`; const [characters, setCharacters] = useState([]); const [speciesOptions, setSpeciesOptions] = useState([]); const [homeworldOptions, setHomeworldOptions] = useState([]); const [fetched, setFetched] = useState(false); useEffect(() => { // 分页拉取所有角色基础数据 const fetchAllCharacters = async () => { let allCharacters = []; let nextUrl = api; while (nextUrl) { const res = await fetch(nextUrl); const data = await res.json(); allCharacters.push(...data.results); nextUrl = data.next; } return allCharacters; }; // 拉取单个角色的所有关联数据,返回处理完成的新角色对象 const processCharacter = async (rawCharacter) => { // 浅拷贝原对象,不修改原始数据 const processedCharacter = { ...rawCharacter }; // 收集所有需要拉取的请求任务 const fetchTasks = []; // 母星处理 fetchTasks.push( fetch(processedCharacter.homeworld) .then(res => res.json()) .then(data => { processedCharacter.homeworld = data.name; }) ); // 物种处理 if (processedCharacter.species.length) { processedCharacter.species.forEach(speciesUrl => { fetchTasks.push( fetch(speciesUrl) .then(res => res.json()) .then(data => { processedCharacter.species = processedCharacter.species.map(u => u === speciesUrl ? data.name : u); }) ); }); } else { processedCharacter.species = ["Unspecified"]; } // 载具处理 if (processedCharacter.vehicles.length) { processedCharacter.vehicles.forEach(vehicleUrl => { fetchTasks.push( fetch(vehicleUrl) .then(res => res.json()) .then(data => { processedCharacter.vehicles = processedCharacter.vehicles.map(u => u === vehicleUrl ? data.name : u); }) ); }); } // 星舰处理 if (processedCharacter.starships.length) { processedCharacter.starships.forEach(starshipUrl => { fetchTasks.push( fetch(starshipUrl) .then(res => res.json()) .then(data => { processedCharacter.starships = processedCharacter.starships.map(u => u === starshipUrl ? data.name : u); }) ); }); } // 等待当前角色所有关联请求完成 await Promise.all(fetchTasks); return processedCharacter; }; // 主流程 const initData = async () => { try { // 1. 拉取全量角色基础数据 const rawCharacters = await fetchAllCharacters(); // 2. 并行处理所有角色的关联数据拉取 const processedCharacters = await Promise.all( rawCharacters.map(char => processCharacter(char)) ); // 3. 生成筛选选项 const speciesSet = new Set(processedCharacters.flatMap(c => c.species)); setSpeciesOptions(Array.from(speciesSet).map(s => <option value={s} key={s}>{s}</option>)); const homeworldSet = new Set(processedCharacters.map(c => c.homeworld)); setHomeworldOptions(Array.from(homeworldSet).map(h => <option value={h} key={h}>{h}</option>)); // 4. 统一更新状态,所有数据就绪后再标记为加载完成 setCharacters(processedCharacters); setFetched(true); } catch (err) { console.error("数据拉取失败:", err); } }; initData(); }, []);
渲染层适配
加载完成前只显示占位符,完全避免原始URL暴露:
if (!fetched) return <div>数据加载中,请稍候...</div>; // 加载完成后再渲染完整页面 return ( <div> <select>{speciesOptions}</select> <select>{homeworldOptions}</select> <table> {/* 渲染处理完成的characters数据即可 */} </table> </div> )
核心优化点
- 所有异步请求都返回Promise,通过多层
Promise.all确保全量数据全部处理完成后才更新状态,不会出现部分数据先渲染的问题 - 所有对象修改都基于浅拷贝,不修改原始数据,避免React状态更新异常
- 分页逻辑用while循环实现,更简洁易维护
- 筛选选项在数据处理完成后直接生成,不需要单独写函数监听state变化,减少不必要的重渲染
内容的提问来源于stack exchange,提问作者Bartosz Król
相关产品推荐
相关产品推荐

