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

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>
}

问题原因分析

  1. 无限重复添加宝可梦:

    • useFetchPokemon Hook顶层直接调用getPokemon,每次组件渲染都会执行该调用,newPokemon更新后触发重渲染,再次调用getPokemon形成循环。
    • useEffect依赖包含input.submited,提交后设为true,清理函数又将其设为false,触发useEffect重复执行,加剧循环。
  2. 应用冻结:

    • 上述无限循环导致组件持续高频重渲染,占用大量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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 17:10:35