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

React中如何在所有数据及图片加载完成后才渲染卡片组件?

解决React PokeAPI卡片数据更新不同步与图片加载缓慢问题

核心问题分析

  1. 未处理pokeObj为空的边界情况,直接解构会导致报错,且无统一加载状态控制,新旧数据交替显示
  2. 图片加载异步,未等待加载完成就渲染,导致更新时图片滞后
  3. useEffect内分散调用setState,触发多次渲染,造成数据不同步
  4. 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

关键修改说明

  1. 全局Loading状态:通过isLoading控制整个卡片的加载流程,确保数据处理和图片加载完成前不会显示旧内容
  2. 图片加载控制:新增imageLoaded状态,配合onLoad/onError事件,避免图片未加载完成就显示
  3. 状态更新优化:将abilityArray的更新移到循环结束后,减少不必要的重渲染;处理pokeObj为空的边界情况
  4. 修复类型图标渲染:直接存储渲染后的组件(<FaBug />)而非构造函数,避免渲染错误
  5. 添加唯一Key:给abilityArray的每一项添加id作为key,消除React列表渲染警告

内容的提问来源于stack exchange,提问作者Hummus Erectus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 03:55:18