如何用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;
关键说明
- 遵循Hook规则:所有Hook调用都在Hook顶层,未在循环或条件中调用。
- 批量请求优化:通过
Promise.all并行发起详情请求,提升加载效率。 - 请求取消:用
AbortController在组件卸载时取消未完成的请求,避免内存泄漏。 - 状态合并:将引用和详情的加载、错误状态合并,对外提供统一接口,简化组件使用。
- 错误处理:区分取消请求的错误与真实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
相关产品推荐
相关产品推荐

