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

如何用React Hooks获取并整合PokeAPI的宝可梦列表与详情?

整合自定义Hook实现批量获取宝可梦数据

问题背景

我正在开发一个调用PokeAPI的React应用,目前已经实现了两个自定义Hook:

  • usePokemonRefs:获取宝可梦引用列表(包含名称与URL),用于后续查询宝可梦信息
  • usePokemonDetails:根据ID获取指定宝可梦的详细信息

PokeAPI的返回机制特殊:调用GET /pokemon接口时,仅返回宝可梦引用(例如{name: "charmander", url: "https://pokeapi.co/pokemon/1"}),而非完整的宝可梦数据。

我希望将这两个Hook整合为一个usePokemon Hook,实现先获取宝可梦列表、再批量获取对应详情的功能,但不确定如何在遵循React Hook规则的前提下高效实现。我是React初学者,现有代码如下:

现有代码

useData.ts

import { useEffect, useState } from "react";
import apiClient from "../services/api-client";

const useData = <T>(endpoint: string, dependencies: any[] = []) => {
  const [data, setData] = useState<T | undefined>(undefined);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    setIsLoading(true);
    setError(null);

    apiClient
      .get<T>(endpoint)
      .then((response) => {
        setData(response.data);
      })
      .catch((err) => {
        setError(err);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, [...dependencies]);

  return { data, isLoading, error };
};

export default useData;

usePokemonRefs.ts

import useData from "./useData";

interface PokemonRef {
  name: string;
  url: string;
}

export interface FetchPokemonResponse {
  count: number;
  next: string;
  previous: string | null;
  results: PokemonRef[];
}

const usePokemonRefs = () => useData<FetchPokemonResponse>("/pokemon");

export default usePokemonRefs;

usePokemonDetails.ts

import useData from "./useData";

export interface Sprites {
  back_default: string | undefined;
  back_female: string | undefined;
  back_shiny: string | undefined;
  back_shiny_female: string | undefined;
  front_default: string | undefined;
  front_female: string | undefined;
  front_shiny: string | undefined;
  front_shiny_female: string | undefined;
}

export interface Pokemon {
  id: number;
  name: string;
  sprites: Sprites;
  types: {
    type: {
      name:
        | "bug"
        | "dark"
        | "dragon"
        | "electric"
        | "fairy"
        | "fighting"
        | "fire"
        | "flying"
        | "ghost"
        | "grass"
        | "ground"
        | "ice"
        | "normal"
        | "poison"
        | "psychic"
        | "rock"
        | "steel"
        | "water";
      url: string;
    };
  }[];
}

const usePokemonDetails = (id: number | string) =>
  useData<Pokemon>(`/pokemon/${id}`, [id]);

export default usePokemonDetails;

解决方案:整合为usePokemon Hook

由于React Hook规则禁止在循环、条件语句中调用Hook,我们不能直接遍历引用列表调用usePokemonDetails。取而代之的是,我们可以在新Hook中先获取引用列表,再通过Promise.all批量调用API获取详情,同时处理加载状态、错误和请求取消逻辑。

实现代码:usePokemon.ts

import { useEffect, useState } from "react";
import usePokemonRefs from "./usePokemonRefs";
import apiClient from "../services/api-client";
import { Pokemon } from "./usePokemonDetails";

const usePokemon = () => {
  // 获取宝可梦引用列表
  const { data: pokemonRefsRes, isLoading: isLoadingRefs, error: refsError } = usePokemonRefs();
  
  // 存储批量获取的宝可梦详情
  const [pokemonList, setPokemonList] = useState<Pokemon[]>([]);
  const [isLoadingDetails, setIsLoadingDetails] = useState(false);
  const [detailsError, setDetailsError] = useState<Error | null>(null);

  useEffect(() => {
    // 如果没有拿到引用列表,直接返回
    if (!pokemonRefsRes?.results) return;

    setIsLoadingDetails(true);
    setDetailsError(null);

    // 创建AbortController用于取消请求,避免内存泄漏
    const abortController = new AbortController();

    // 批量请求每个宝可梦的详情:从URL中提取ID
    const fetchPokemonDetails = async () => {
      try {
        const pokemonPromises = pokemonRefsRes.results.map(ref => {
          // 从URL中提取ID:例如"https://pokeapi.co/pokemon/1" -> "1"
          const id = ref.url.split("/").filter(Boolean).pop();
          return apiClient.get<Pokemon>(`/pokemon/${id}`, { signal: abortController.signal }).then(res => res.data);
        });

        const pokemonDetails = await Promise.all(pokemonPromises);
        setPokemonList(pokemonDetails);
      } catch (err) {
        // 忽略取消请求的错误
        if (err instanceof Error && err.name !== "AbortError") {
          setDetailsError(err);
        }
      } finally {
        setIsLoadingDetails(false);
      }
    };

    fetchPokemonDetails();

    // 组件卸载时取消请求
    return () => abortController.abort();
  }, [pokemonRefsRes?.results]);

  // 合并加载状态:只要引用或详情在加载,整体就处于加载中
  const isLoading = isLoadingRefs || isLoadingDetails;
  // 合并错误信息:优先显示详情请求的错误,没有则显示引用请求的错误
  const error = detailsError || refsError;

  return { pokemonList, isLoading, error };
};

export default usePokemon;

关键说明

  1. 遵循Hook规则:所有Hook调用都在Hook顶层,未在循环或条件中调用。
  2. 批量请求优化:通过Promise.all并行发起详情请求,提升加载效率。
  3. 请求取消:用AbortController在组件卸载时取消未完成的请求,避免内存泄漏。
  4. 状态合并:将引用和详情的加载、错误状态合并,对外提供统一接口,简化组件使用。
  5. 错误处理:区分取消请求的错误与真实API错误,避免误报。

进阶优化:添加缓存

若要避免重复请求相同宝可梦详情,可加入缓存机制:

// 在usePokemon内部添加缓存状态
const [pokemonCache, setPokemonCache] = useState<Record<string, Pokemon>>({});

// 修改fetchPokemonDetails中的逻辑
const pokemonPromises = pokemonRefsRes.results.map(ref => {
  const id = ref.url.split("/").filter(Boolean).pop();
  if (pokemonCache[id]) {
    return Promise.resolve(pokemonCache[id]);
  }
  return apiClient.get<Pokemon>(`/pokemon/${id}`, { signal: abortController.signal })
    .then(res => {
      const data = res.data;
      setPokemonCache(prev => ({ ...prev, [id]: data }));
      return data;
    });
});

后续再获取相同ID的宝可梦时,会直接使用缓存数据,减少API调用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 00:19:51