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

合并两个API数据后React组件无法渲染的问题求解

问题:接口数据合并后无法触发组件渲染,添加依赖引发无限循环

请求两个接口并将数据存入state后,无法在状态变更时渲染组件。若将相关状态变量加入useEffect的依赖数组,会引发无限渲染。目前仅能通过将合并后的状态加入依赖数组实现组件加载后的渲染,但这并非合理方案。

组件代码如下:

import { ChangeEvent, FC, useEffect, useState } from "react";
import spacex from "../api/spacex";
import CardGrid from "../components/CardGrid";
import Header from "../components/Header";
import Pagination from "../components/Pagination";
import SkeletonGrid from "../components/SkeletonGrid";

type Launch = {
    mission_name: string;
};

const LaunchesMain: FC = () => {
    const [launches, setLaunches] = useState<any>([]);
    const [rockets, setRockets] = useState<any>([]);
    const [merged, setMerged] = useState([]);
    const [currentPage, setCurrentPage] = useState(1);
    const [postsPerPage, setPostsPerPage] = useState(9);
    const [searchTerm, setSearchTerm] = useState("");
    const [filteredResult, setFilteredResult] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        const fetchRockets = async () => {
            const responseRocket = await spacex.get("/rockets");
            const responseLaunches = await spacex.get("/launches");

            setRockets(responseRocket.data);
            setLaunches(responseLaunches.data);
        };
        fetchRockets().then(() => {
            const mergedApis = () => {
                const launchesCopy: any = [...launches];

                for (let i = 0; i < launches.length; i++) {
                    for (let j = 0; j < rockets.length; j++) {
                        if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
                            launchesCopy[i].rocket = rockets[j];
                        }
                    }
                }
                setMerged(launchesCopy);
                setIsLoading(false);
            };
            mergedApis();
        });
    }, []);

    console.log(merged);

    const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
        setSearchTerm(event.target.value);

        if (searchTerm.length === 0) {
            setFilteredResult(merged);
        } else if (searchTerm.length > 0) {
            const filteredData = merged.filter((launch: Launch) => {
                return `${launch.mission_name}`
                    .toLowerCase()
                    .includes(searchTerm.toLowerCase());
            });
            setFilteredResult(filteredData);
        }
    };

    const lastPostIndex = currentPage * postsPerPage;
    const firstPostIndex = lastPostIndex - postsPerPage;
    const currentPosts = merged.slice(firstPostIndex, lastPostIndex);

    return (
        <>
            <Header />
            <div className="text-white">
                <div>
                    <input
                        style={{
                            background:
                                "linear-gradient(0deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)), #121212",
                        }}
                        onChange={(event) => handleSearchChange(event)}
                        placeholder="Search all launches..."
                        value={searchTerm}
                        className="md:w-[26rem] w-[16rem] h-[3rem] rounded-lg mt-10 mx-5 md:mx-24 rounded-3"
                    />
                </div>
                <div className="mx-5 md:ml-24 mt-5 opacity-40">
                    Total({currentPosts.length})
                </div>
                {isLoading ? (
                    <SkeletonGrid cards={postsPerPage} />
                ) : (
                    <CardGrid
                        postsData={currentPosts}
                        filteredResult={filteredResult}
                        searchTerm={searchTerm}
                    />
                )}

                <Pagination
                    totalPosts={merged.length}
                    postsPerPage={postsPerPage}
                    setCurrentPage={setCurrentPage}
                    currentPage={currentPage}
                />
            </div>
        </>
    );
};

export default LaunchesMain;

核心问题分析

代码中存在两个关键问题:

  1. fetchRockets的then回调里调用mergedApis时,直接读取了组件当前的launches和rockets状态——但React的setState是异步操作,此时这两个状态还未被接口返回的数据更新,合并逻辑拿到的是空数组,导致merged始终为空,无法触发渲染。
  2. 若将launches/rockets加入useEffect依赖数组,每次状态更新都会重新执行请求和合并逻辑,再次触发状态更新,形成无限循环。

解决方案

1. 直接用接口返回数据完成合并,跳过中间状态

异步请求完成后,直接使用接口返回的原始数据做合并,不需要先存入组件状态再读取,彻底避免异步状态更新的延迟问题。

修改后的useEffect代码:

useEffect(() => {
  const fetchAndMergeData = async () => {
    try {
      setIsLoading(true);
      // 并行请求两个接口,提升性能
      const [responseRocket, responseLaunches] = await Promise.all([
        spacex.get("/rockets"),
        spacex.get("/launches")
      ]);
      
      const rocketsData = responseRocket.data;
      const launchesData = responseLaunches.data;

      // 用map+find替代嵌套循环,更高效清晰
      const mergedData = launchesData.map(launch => {
        const matchedRocket = rocketsData.find(rocket => 
          launch.rocket.rocket_name === rocket.rocket_name
        );
        // 找不到匹配时保留原rocket数据,避免报错
        return {...launch, rocket: matchedRocket || launch.rocket};
      });

      setMerged(mergedData);
      setIsLoading(false);
    } catch (error) {
      console.error("数据请求/合并失败:", error);
      setIsLoading(false);
    }
  };

  fetchAndMergeData();
}, []);

2. 移除不必要的状态变量

既然不需要单独存储launches和rockets状态,直接删除对应的useState声明:

// 删除这两行代码
const [launches, setLaunches] = useState<any>([]);
const [rockets, setRockets] = useState<any>([]);

3. 修复搜索逻辑的异步状态问题

setSearchTerm是异步操作,直接在之后读取searchTerm会拿到旧值,导致搜索逻辑出错。改用event.target.value直接处理:

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
  const inputValue = event.target.value.toLowerCase();
  setSearchTerm(inputValue);

  if (inputValue.length === 0) {
    setFilteredResult(merged);
  } else {
    const filteredData = merged.filter((launch: Launch) => 
      launch.mission_name.toLowerCase().includes(inputValue)
    );
    setFilteredResult(filteredData);
  }
};

4. 优化分页与统计逻辑(可选)

让分页和统计数据根据搜索状态动态切换,使用过滤后的数据或原始合并数据:

// 动态选择要展示的数据
const displayData = searchTerm ? filteredResult : merged;
const lastPostIndex = currentPage * postsPerPage;
const firstPostIndex = lastPostIndex - postsPerPage;
const currentPosts = displayData.slice(firstPostIndex, lastPostIndex);

同时更新统计文本:

<div className="mx-5 md:ml-24 mt-5 opacity-40">
  Total({displayData.length})
</div>

方案生效原因

  • 直接使用接口返回的数据合并,确保合并逻辑拿到完整的接口数据,不会因React异步状态更新导致读取旧值。
  • useEffect依赖数组保持为空,仅在组件挂载时执行一次请求和合并,不会引发无限循环。
  • 修复搜索逻辑的异步状态问题,保证搜索结果的正确性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 09:41:01