如何解决React useEffect调用PokeAPI时的无限循环问题
解决方案:修复无限循环与优化性能
问题根源
- 无限循环原因:你的
useEffect将myPokemonsList设为依赖,但内部的getAllPokemons函数会调用setMyPokemonsList更新该状态,导致依赖变化后再次触发useEffect,形成无限循环。 - 冗余异步操作:
filterMyPokemons标记为async,但Array.filter是同步方法,await完全多余,反而可能引发不必要的异步逻辑。 - 不必要的状态存储:
pokemonListFiltered属于派生状态(可由pokemonList和myPokemonsList计算得出),单独存储会增加状态同步成本,还可能导致数据不一致。
具体修复步骤
1. 重构useEffect,消除无限循环
将useEffect的依赖设为空数组,仅在组件挂载时执行一次数据获取;同时用Promise.all并行请求,提升加载效率:
useEffect(() => { const fetchData = async () => { setLoading(true); // 并行请求,减少等待时间 const [myPokemons, pokemon] = await Promise.all([ showMyPokemons(), api.getAllPokemon() ]); setPokemonList(pokemon); setMyPokemonsList(myPokemons); setLoading(false); } fetchData(); }, []); // 空依赖,仅组件挂载时执行
2. 用useMemo派生过滤列表,替代状态存储
删除pokemonListFiltered状态和filterMyPokemons函数,改用useMemo缓存过滤结果,避免重复计算:
// 过滤掉已捕获的宝可梦 const pokemonListFiltered = useMemo(() => { return pokemonList.filter(pokemon => !myPokemonsList.includes(pokemon.name)); }, [pokemonList, myPokemonsList]); // 仅当依赖变化时重新计算 // 搜索过滤也用useMemo缓存 const filteredPokemons = useMemo(() => { const lowerSearch = text.toLowerCase(); return pokemonListFiltered.filter(pokemon => pokemon.name.toLowerCase().includes(lowerSearch) ); }, [pokemonListFiltered, text]);
3. (可选)监听myPokemons实时变化
如果你的CatchContext中直接暴露了myPokemons状态(而非仅通过showMyPokemons函数获取),可以将其加入useEffect依赖,实现实时更新已捕获列表:
// 假设Context提供myPokemons状态 const { myPokemons } = useContext(CatchContext); // 修改useEffect依赖 useEffect(() => { const fetchData = async () => { setLoading(true); const pokemon = await api.getAllPokemon(); setPokemonList(pokemon); setMyPokemonsList(myPokemons); setLoading(false); } fetchData(); }, [myPokemons]); // 当myPokemons变化时重新获取数据(如果需要)
修改后的完整代码
import * as C from './styles'; import logo from '../../assets/pokelogo.png'; import { useContext, useState, useEffect, useMemo } from 'react'; import { useApi } from '../../hooks/useApi'; import { useNavigate } from 'react-router-dom'; import Pokelist from '../../components/PokeList'; import CatchingPokemonIcon from '@mui/icons-material/CatchingPokemon'; import CatchContext from '../../context/Context'; const Homepage = () => { const api = useApi(); const { showMyPokemons } = useContext(CatchContext); const navigate = useNavigate(); const [pokemonList, setPokemonList] = useState([]); const [loading, setLoading] = useState(false); const [text, setText] = useState(''); const [myPokemonsList, setMyPokemonsList] = useState([]); useEffect(() => { const fetchData = async () => { setLoading(true); const [myPokemons, pokemon] = await Promise.all([ showMyPokemons(), api.getAllPokemon() ]); setPokemonList(pokemon); setMyPokemonsList(myPokemons); setLoading(false); } fetchData(); }, []); // 派生过滤后的列表 const pokemonListFiltered = useMemo(() => { return pokemonList.filter(pokemon => !myPokemonsList.includes(pokemon.name)); }, [pokemonList, myPokemonsList]); // 搜索过滤 const filteredPokemons = useMemo(() => { const lowerSearch = text.toLowerCase(); return pokemonListFiltered.filter(pokemon => pokemon.name.toLowerCase().includes(lowerSearch) ); }, [pokemonListFiltered, text]); const handleHome = () => { navigate('/') } const handleMyPokemons = () => { navigate('/mypokemons') } return ( <C.Container> <C.Logo> <img src={logo} alt="宝可梦Logo" /> </C.Logo> <C.Navbar> <input type="text" placeholder='Busque um pokémon...' onChange={(e) => setText(e.target.value)} value={text} /> </C.Navbar> <C.Pokedatabase onClick={handleMyPokemons}> <button>Meus pokémons <i><CatchingPokemonIcon /></i></button> </C.Pokedatabase> {loading ? ( <div>加载中...</div> ) : ( <C.Pokelist> {filteredPokemons.map(pokemon => ( <Pokelist key={pokemon.name} // 添加key,避免React警告 name={pokemon.name} /> ))} </C.Pokelist> )} </C.Container> ) } export default Homepage;
额外优化点
- 添加了
loading状态的渲染逻辑,提升用户体验 - 给
map循环的元素添加了key,消除React控制台警告 - 用
Promise.all并行请求,减少数据加载等待时间
内容的提问来源于stack exchange,提问作者Routfin
相关产品推荐
相关产品推荐

