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

React中.map()遍历调用async异步函数渲染异常问题求助

问题根因
  • 所有async函数的返回值都是Promise对象,直接在JSX渲染位置调用getBpm(track.id)拿到的是pending状态的Promise实例,React不会自动等待Promise解析完成再取实际值渲染,因此无法正常显示BPM数值。
  • 原错误处理逻辑存在缺陷:如果axios请求报错进入catch分支,没有返回任何兜底值,后续执行data[0]?.audio_features[0].tempo时会因为data为undefined直接抛出运行时错误。
  • 逐track单独发起请求的效率极低,Spotify的/audio-features接口单次最多支持传入100个逗号分隔的track id,循环单条请求会浪费接口配额,提升触发限流的概率。
推荐解决方案(批量拉取,性能最优)

优先使用批量拉取方案,在组件侧统一拉取所有BPM数据存入状态,再渲染列表,请求数少、逻辑可控。代码示例如下:

import { useState, useEffect } from 'react';
import axios from 'axios';

// 批量拉取BPM,修正原接口返回值解析错误
const fetchAllBpms = async (trackIds, token) => {
  try {
    const { data } = await axios.get("https://api.spotify.com/v1/audio-features", {
      headers: {
        Authorization: `Bearer ${token}`
      },
      params: {
        // 接口支持多id用逗号拼接,单次最多传100个
        ids: trackIds.join(','),
      }
    });
    // 构造id -> bpm的映射表,缺失值返回null兜底
    const bpmMap = {};
    data.audio_features.forEach(feature => {
      if (feature) {
        bpmMap[feature.id] = Math.round(feature.tempo);
      }
    });
    return bpmMap;
  } catch (error) {
    console.log('拉取BPM数据失败:', error);
    return {};
  }
}

function TrackList({ tracks, token }) {
  const [bpmMap, setBpmMap] = useState({});
  const [bpmLoading, setBpmLoading] = useState(true);

  // 组件挂载、tracks变化时批量拉取BPM
  useEffect(() => {
    if (!tracks?.length) {
      setBpmLoading(false);
      return;
    }
    setBpmLoading(true);
    const trackIds = tracks.map(track => track.id);
    fetchAllBpms(trackIds, token).then(map => {
      setBpmMap(map);
      setBpmLoading(false);
    });
  }, [tracks, token]);

  return (
    <ul>
      {tracks?.map((track) => 
        <li key={track.id} className="song-item">
          <a className='link-to-song' href="">
            <div className="img-with-data">
              <img src={track.album.images[0].url} alt={track.name} />
              <div className="title-artist">
                <p className='song-title'>{track.name}</p>
                <p className='song-artist'>{track.artists[0].name}</p>
              </div>
            </div>
            <p className="tempo">
              {bpmLoading ? '加载中...' : bpmMap[track.id] || '无数据'}
            </p>
          </a>
        </li>
      )}
    </ul>
  )
}
备选方案(单条独立请求,适合懒加载场景)

如果列表很长、做了虚拟滚动/懒加载,不需要一次性加载所有BPM,可以把BPM渲染逻辑拆成独立组件,每个组件自己管理请求和状态,代码示例如下:

import { useState, useEffect } from 'react';
import axios from 'axios';

// 独立BPM组件,自行管理异步请求状态
function TrackBpm({ trackId, token }) {
  const [bpm, setBpm] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let canceled = false;
    setLoading(true);
    const getSingleBpm = async () => {
      try {
        const { data } = await axios.get("https://api.spotify.com/v1/audio-features", {
          headers: { Authorization: `Bearer ${token}` },
          params: { ids: trackId }
        });
        if (!canceled) {
          const tempo = data.audio_features?.[0]?.tempo;
          setBpm(tempo ? Math.round(tempo) : null);
        }
      } catch (err) {
        console.log('拉取单首BPM失败:', err);
        if (!canceled) setBpm(null);
      } finally {
        if (!canceled) setLoading(false);
      }
    };
    getSingleBpm();
    // 组件卸载时终止状态更新,避免内存泄漏
    return () => { canceled = true };
  }, [trackId, token]);

  if (loading) return <span>加载中...</span>;
  return <span>{bpm || '无数据'}</span>;
}

// 列表渲染直接调用独立组件
function renderTracks() {
  return tracks?.map((track) => 
    <li key={track.id} className="song-item">
      <a className='link-to-song' href="">
        <div className="img-with-data">
          <img src={track.album.images[0].url} alt={track.name} />
          <div className="title-artist">
            <p className='song-title'>{track.name}</p>
            <p className='song-artist'>{track.artists[0].name}</p>
          </div>
        </div>
        <p className="tempo">
          <TrackBpm trackId={track.id} token={token} />
        </p>
      </a>
    </li>
  )
}
注意事项
  • 原代码存在接口返回值解析错误:Spotify /audio-features 接口的响应体直接包含audio_features数组,不需要取data[0],原写法即使Promise正常执行也无法拿到正确tempo值。
  • 批量拉取时如果track总数超过100,需要将id数组按每100个一组分片请求,再合并结果,避免触发接口参数长度限制。
  • 禁止在渲染函数执行过程中直接触发异步请求,否则会导致重复请求、渲染死循环、状态不一致等问题,异步逻辑必须放在副作用钩子或独立组件的生命周期内执行。

内容的提问来源于stack exchange,提问作者michal-kow

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 07:16:01