React中如何在所有数据及图片加载完成后才渲染卡片组件?
解决React PokeAPI卡片数据更新不同步与图片加载缓慢问题
核心问题分析
- 未处理
pokeObj为空的边界情况,直接解构会导致报错,且无统一加载状态控制,新旧数据交替显示 - 图片加载异步,未等待加载完成就渲染,导致更新时图片滞后
useEffect内分散调用setState,触发多次渲染,造成数据不同步abilityArray在循环内频繁更新状态,引发不必要的重渲染
修复方案与代码实现
以下是修改后的完整代码,关键修改处已标注:
import { useState, useEffect } from "react" // Icon imports here ... // Background imports here ... function PokeCard(props) { const { pokeObj } = props // 修改:处理pokeObj为空的情况,避免解构报错 const { pokemon, species, abilities } = pokeObj || {} const [description, setDescription] = useState('') const [bgImage, setBgImage] = useState('') const [typeIcon, setTypeIcon] = useState('') const [circleColor, setCircleColor] = useState('') const [abilityArray, setAbilityArray] = useState([]) // 新增:全局加载状态与图片加载状态 const [isLoading, setIsLoading] = useState(true) const [imageLoaded, setImageLoaded] = useState(false) useEffect(() => { // 每次pokeObj更新时重置加载状态 setIsLoading(true) setImageLoaded(false) setAbilityArray([]) setDescription('') if (!pokeObj) { setIsLoading(false) return } if (pokemon.id > 905) { setIsLoading(false) return } // 处理描述文本 const flavorArray = species.flavor_text_entries const desc = flavorArray.filter(flavor => flavor.language.name === 'en') let formattedDesc = desc[0].flavor_text .replace('',' ') .replace('\n', '') .replace(/\b[A-Z]+\b/g, (word) => word[0].toUpperCase() + word.slice(1).toLowerCase()) setDescription(formattedDesc) // 处理类型装饰 const getTypeDecoration = () => { let bg let circ let icon switch (pokemon.types[0].type.name) { case 'bug': bg = bgGreen circ = "bg-green-700" icon = <FaBug /> // 修改:直接渲染组件,而非存储组件构造函数 break // Other cases here ... } setBgImage(bg) setTypeIcon(icon) setCircleColor(circ) } getTypeDecoration() // 处理能力数据 const newAbilities = [] for (const a of abilities) { const abilityObj = {} const abilityArr = a.names const name = abilityArr.filter(name => name.language.name === 'en') abilityObj.abilityName = name[0].name abilityObj.id = a.id // 新增:添加唯一key,避免map警告 const abilityFlavorArray = a.flavor_text_entries const desc = abilityFlavorArray.filter(flavor => flavor.language.name === 'en') let formattedDesc = desc[0].flavor_text .replace(/\b[A-Z]+\b/g, (word) => word[0].toUpperCase() + word.slice(1).toLowerCase()) .replace('Pp', 'PP') .replace('Hp', 'HP') abilityObj.abilityDescription = formattedDesc newAbilities.push(abilityObj) } // 修改:循环结束后一次性更新状态,减少重渲染 setAbilityArray(newAbilities) // 数据处理完成,等待图片加载 setIsLoading(false) }, [pokeObj]) // 图片加载完成回调 const handleImageLoad = () => { setImageLoaded(true) } // 图片加载失败回调 const handleImageError = () => { setImageLoaded(true) // 加载失败也显示占位,避免一直loading } return ( <div> {/* 优先显示加载状态 */} {isLoading && ( <div className="container mx-auto p-5 m-10 bg-yellow-300 rounded-3xl w-96 h-[600px] flex items-center justify-center"> <div className="text-xl font-futura">Loading Pokémon...</div> </div> )} {/* 加载完成后渲染卡片 */} {!isLoading && pokemon?.id <= 905 ? ( <div className="container mx-auto card-outer p-5 m-10 bg-yellow-300 rounded-3xl text-black font-futura w-96"> <div style={{ backgroundImage: `url(${bgImage})` }} className="card-inner p-3 bg-hero bg-no-repeat bg-cover bg-center"> <div className="card-top flex text-2xl mx-3"> <div className="name grow text-3xl">{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</div> <div className="pokedexNumber text-orange-600 ">#{pokemon.id}</div> <div className={`type-button w-fit h-fit p-1 rounded-full ml-1 ${circleColor}`}> {typeIcon} </div> </div> <div className="picture-border-container mx-3 bg-gradient-to-br from-yellow-200 via-yellow-600 to-yellow-200 p-2 drop-shadow-md"> <div className="card-picture bg-orange-200 h-48 flex items-center justify-center"> {/* 修改:图片加载完成前显示占位,加载完成后显示图片 */} {!imageLoaded && <div className="text-lg">Loading Image...</div>} <img className={`h-full mx-auto ${!imageLoaded ? 'hidden' : ''}`} src={pokemon.sprites.other['official-artwork'].front_default} alt={pokemon.name} onLoad={handleImageLoad} onError={handleImageError} /> </div> </div> <div className="under-image"> <div className="generation-box"> <svg width="40" height="40" viewBox="30 0 350 350" className="inline"> <defs> <path id="MyPath" d="M 200, 200 m -100, 0 a 100,100 0 1,1 200,0 a 100,100 0 1,1 -200,0" /> </defs> <text className="text-[70px]"> <textPath xlinkHref="#MyPath"> GENERATION </textPath> </text> <circle fill="black" cx="200" cy="200" r="80" strokeWidth="10" /> <text x="200" y="205" textAnchor="middle" stroke="white" fill="white" strokeWidth="1px" alignmentBaseline="middle" className="text-[90px]" > {species?.generation.url.charAt(species.generation.url.length - 2)} </text> </svg> <p className="info-strip inline-block bg-gradient-to-br from-yellow-600 via-yellow-400 to-yellow-600 w-60 italic text-xs text-center" >Height: {pokemon.height/10}m, Weight: {pokemon.weight/10}kg</p> </div> </div> <div className="mb-4 h-[160px]"> {abilityArray.length > 0 && abilityArray.map((a) => ( <React.Fragment key={a.id}> <div className="ability-box mx-10 h-20 flex items-center " > <div className="ability-name inline text-lg leading-3"> {a.abilityName} <span className="ability-desc text-sm pl-3">{a.abilityDescription}</span> </div> </div> <hr className="border-1 border-black"/> </React.Fragment> ))} </div> <div className="pokemon-description border-2 text-xs border-yellow-500 mx-3 leading-4 " > {description} </div> <div className="copyright-info text-[10px] text-center"> Pokémon and Pokémon character names are trademarks of Nintendo. </div> </div> </div> ) : !isLoading ? ( <p>Oops! Couldn't find that Pokémon, sorry! </p> ) : null} </div> ) } export default PokeCard
关键修改说明
- 全局Loading状态:通过
isLoading控制整个卡片的加载流程,确保数据处理和图片加载完成前不会显示旧内容 - 图片加载控制:新增
imageLoaded状态,配合onLoad/onError事件,避免图片未加载完成就显示 - 状态更新优化:将
abilityArray的更新移到循环结束后,减少不必要的重渲染;处理pokeObj为空的边界情况 - 修复类型图标渲染:直接存储渲染后的组件(
<FaBug />)而非构造函数,避免渲染错误 - 添加唯一Key:给
abilityArray的每一项添加id作为key,消除React列表渲染警告
内容的提问来源于stack exchange,提问作者Hummus Erectus
相关产品推荐
相关产品推荐

