React Pokemon API应用问题:无限添加与输入冻结排查
React Pokemon API应用问题排查与修复
问题描述
- 发起一次请求获取宝可梦时,会无限重复添加同一只宝可梦
- 再次输入内容时应用会完全冻结
相关代码
App.tsx
import React from 'react' import './App.css' import {PokemonForm} from '@components/PokemonForm/PokemonForm' import {PokemonProvider} from "@context/PokemonContext"; import {PokemonList} from "@components/PokemonForm/PokemonList"; const App: React.FC = () => { return ( <PokemonProvider> <PokemonList/> <PokemonForm/> </PokemonProvider> ) } export default App
PokemonForm.tsx(表单组件)
import React from "react"; import {useFetchPokemon} from '@hooks/useFetchPokemon' export const PokemonForm: React.FC = () => { const {input, handleChange, handleSubmit} = useFetchPokemon() return ( <form className="sfr-form" onSubmit={handleSubmit}> <input name="name" value={input.name} placeholder="输入宝可梦名称" onChange={(e)=> handleChange(e)}/> <button type="submit">获取</button> </form> ); };
useFetchPokemon.ts(请求Hook)
import React, {useState, useEffect} from 'react' import {usePokemon} from "@context/PokemonContext"; export const useFetchPokemon = () => { const [newPokemon, setNewPokemon] = useState<Pokemon | string | null>(() => ''); const [input, setInput] = useState({name: '', submited: false}) const {getPokemon} = usePokemon() getPokemon(newPokemon as Pokemon) useEffect(() => { const getData = async () => { try{ const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${input.name}`).then((res)=>res.json()) setNewPokemon({id: response.id, name: response.name, type:response.types[0].type.name, imageUrl: response.species.url}) } catch (error) { console.log('error', error) } }; getData(); return () => { setNewPokemon('') setInput({name: '', submited: false}) }; }, [input.name, input.submited]); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInput({...input, name: event.target.value}) } const handleSubmit = async (event: { preventDefault: () => void; }) => { event.preventDefault(); setInput({...input, submited: true}); } return {newPokemon, handleChange, handleSubmit, input} }
useLocalStorage.ts(本地存储Hook)
import { useEffect, useState } from "react" export function useLocalStorage<T>(key: string, initialValue: T | (() => T)) { const [value, setValue] = useState<T>(() => { const local = localStorage.getItem(key) if (local != null) return JSON.parse(local) return typeof initialValue === "function" ? (initialValue as () => T)() : initialValue }) useEffect(() => { localStorage.setItem(key, JSON.stringify(value)) }, [key, value]) return [value, setValue] as [typeof value, typeof setValue] }
PokemonList.tsx(列表组件)
import React from "react"; import {usePokemon} from "@context/PokemonContext"; export const PokemonList = () => { const {pokemon} = usePokemon() return ( <div className="sfr-list"> {pokemon && [pokemon].map((e, i) => ( <div key={i}> <pre> {JSON.stringify(e, null, 2)} </pre> </div> ))} </div> ) }
PokemonContext.tsx(上下文)
import {createContext, ReactNode, useContext} from "react"; import {useLocalStorage} from "@hooks/useFetchStorage"; type PokemonContext = { pokemon: {}; getPokemon: ({} : Pokemon) => void } const PokemonContext = createContext({} as PokemonContext) export const usePokemon = () => { return useContext(PokemonContext) } export const PokemonProvider = ({children} : {children: ReactNode}) => { const [pokemon, setPokemon] = useLocalStorage<Pokemon[]>('pokemon', [{id: 123, name: 'mariomon', type: 'fogo', imageUrl: 'www.google.com'}]); const getPokemon = async (newlyPokemon : Pokemon) => { await newlyPokemon && setPokemon(currentPokemons => [...currentPokemons, newlyPokemon]) } return <PokemonContext.Provider value={{getPokemon, pokemon}}> {children} </PokemonContext.Provider> }
问题原因分析
无限重复添加宝可梦:
useFetchPokemonHook顶层直接调用getPokemon,每次组件渲染都会执行该调用,newPokemon更新后触发重渲染,再次调用getPokemon形成循环。useEffect依赖包含input.submited,提交后设为true,清理函数又将其设为false,触发useEffect重复执行,加剧循环。
应用冻结:
- 上述无限循环导致组件持续高频重渲染,占用大量CPU资源,最终使应用无响应。
PokemonList中将数组pokemon包装成[pokemon]再遍历,会把整个数组作为单个元素重复渲染,增加不必要的计算负载。
修复方案
1. 重构useFetchPokemon Hook
移除顶层的getPokemon调用,改为在获取数据成功后调用;调整useEffect逻辑,避免循环触发;仅在提交成功后重置表单。
import React, {useState, useEffect} from 'react' import {usePokemon} from "@context/PokemonContext"; export const useFetchPokemon = () => { const [input, setInput] = useState({name: '', submitted: false}) const {getPokemon} = usePokemon() useEffect(() => { if (!input.submitted || !input.name.trim()) return; const getData = async () => { try{ const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${input.name}`); const data = await response.json(); const newPokemon = { id: data.id, name: data.name, type: data.types[0].type.name, imageUrl: data.sprites.front_default // 修正为正确的图片字段 }; await getPokemon(newPokemon); setInput({name: '', submitted: false}); } catch (error) { console.log('获取失败', error); setInput(prev => ({...prev, submitted: false})); } }; getData(); }, [input.submitted, input.name, getPokemon]); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInput({...input, name: event.target.value}) } const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (input.name.trim()) { setInput({...input, submitted: true}); } } return {handleChange, handleSubmit, input} }
2. 修复PokemonList组件
直接遍历pokemon数组,使用宝可梦id作为key,避免索引key的问题。
import React from "react"; import {usePokemon} from "@context/PokemonContext"; export const PokemonList = () => { const {pokemon} = usePokemon() return ( <div className="sfr-list"> {pokemon.map((poke) => ( <div key={poke.id}> <pre> {JSON.stringify(poke, null, 2)} </pre> </div> ))} </div> ) }
3. 修正Context类型与逻辑
完善类型定义,添加重复宝可梦过滤逻辑,避免重复添加。
import {createContext, ReactNode, useContext} from "react"; import {useLocalStorage} from "@hooks/useLocalStorage"; // 修正导入路径 type Pokemon = { id: number; name: string; type: string; imageUrl: string; } type PokemonContextType = { pokemon: Pokemon[]; getPokemon: (newPokemon: Pokemon) => Promise<void> } const PokemonContext = createContext<PokemonContextType>({} as PokemonContextType) export const usePokemon = () => { return useContext(PokemonContext) } export const PokemonProvider = ({children} : {children: ReactNode}) => { const [pokemon, setPokemon] = useLocalStorage<Pokemon[]>('pokemon', [{id: 123, name: 'mariomon', type: 'fogo', imageUrl: 'www.google.com'}]); const getPokemon = async (newlyPokemon: Pokemon) => { if (newlyPokemon) { const exists = pokemon.some(poke => poke.id === newlyPokemon.id); if (!exists) { setPokemon(current => [...current, newlyPokemon]); } } } return <PokemonContext.Provider value={{getPokemon, pokemon}}> {children} </PokemonContext.Provider> }
修复后效果
- 提交请求后仅添加一次宝可梦,无重复
- 再次输入内容时应用正常响应,无冻结
- 自动过滤重复宝可梦,列表数据唯一
内容的提问来源于stack exchange,提问作者asd123
相关产品推荐
相关产品推荐

