如何在含React useEffect钩子的Promise函数中使用await关键字
我正在开发一个项目,需要通过Spotify API获取歌曲ID。我想调用一个返回Promise(包含Spotify歌曲ID列表)的函数,用await等待它完成,再用返回的数据创建新播放列表。
当我在非await上下文调用CurrentPlaylists()并让它返回歌曲数组时,函数运行正常。但尝试让它返回Promise并等待执行完毕时,出现了以下错误:
Uncaught Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
以下是我的代码:
调用CurrentPlaylists()并等待返回的函数
//top level function export const MergePlaylists = async () => { const CurrentSongList = await CurrentPlaylists(); console.log("Cur: " + CurrentSongList.length); }
获取当前用户所有播放列表歌曲ID并返回Promise的函数
function CurrentPlaylists() { const [playlistsData, setPlaylistsData] = useState(null); const [playlists, setPlaylists] = useState(null); const PIDS = []; const songIDs = []; //retrieve first 20 playlists useEffect(() => { const fetchData = async () => { const { data } = await getCurrentUserPlaylists(); setPlaylists(playlists => ([ ...playlists ? playlists : [], ...data.items ])); setPlaylistsData(data); }; //console.log(playlistsData); catchErrors(fetchData()); }, []); // When playlistsData updates, check if there are more playlists to fetch // then update the state variable useEffect(() => { if (!playlistsData) { return; } // Playlist endpoint only returns 20 playlists at a time, so we need to // make sure we get ALL playlists by fetching the next set of playlists const fetchMoreData = async () => { if (playlistsData.next) { //as long as there is more data to be retrieved const { data } = await axios.get(playlistsData.next); setPlaylists(playlists => ([ ...playlists ? playlists : [], ...data.items ])); setPlaylistsData(data); } else { if(playlists !== null) { //extract playlist ID from playlist into array Object.entries(playlists).forEach((key, value) => { const PID = key[1]["id"]; if (!PIDS.includes(PID)) { PIDS.push(PID); } }); //for each playlist, for (var PID in PIDS) { const id = PIDS[PID]; //listData contains first 100 songs let listData = await getPlaylistItems(id); //retrieve songs from specified playlist Object.entries(listData.data.items).forEach((key, value) => { const songID = key[1]["track"]["id"]; if (!songIDs.includes(songID) && songID !== null) { songIDs.push(songID); } }); //console.log("before while: " + i); songIDs.push(listData.data.items); while (listData.data.next) { listData = await axios.get(listData.data.next); console.log("nexted"); Object.entries(listData.data.items).forEach((key, value) => { const songID = key[1]["track"]["id"]; if (!songIDs.includes(songID) && songID !== null) { songIDs.push(songID); } }) songIDs.push(listData.data.items); } } console.log(songIDs.length); //return a promise containing the array of songIDs return new Promise((resolve, reject) => { resolve(songIDs); }); } } }; catchErrors(fetchMoreData()); }, [playlistsData]); };
CurrentPlaylists是一个React组件(因为它使用了useState、useEffect这些React Hooks),组件的本质是返回React元素的函数,不能像普通异步函数那样返回Promise。当你用await CurrentPlaylists()时,实际上是在等待一个React组件实例,而非你期望的歌曲ID数组Promise,这就触发了React的错误——不能把Promise作为React子元素渲染。
另外,组件内部useEffect中的异步函数返回的Promise无法传递到组件外部,因为useEffect的回调函数返回值要么是清理函数,要么是undefined,内部的返回值不会被组件本身接收。
把数据获取逻辑从React组件中抽离出来,写成独立的异步函数,完全脱离React Hooks的约束。如果需要在组件中使用这些数据,再在组件内部调用这个函数并更新状态。
修改后的独立数据获取函数
// 独立的异步函数,负责获取所有歌曲ID export async function getAllUserSongIDs() { const songIDs = []; const PIDS = []; // 获取第一页播放列表 let { data: playlistsData } = await getCurrentUserPlaylists(); let allPlaylists = [...playlistsData.items]; // 循环获取所有播放列表(处理分页) while (playlistsData.next) { const { data } = await axios.get(playlistsData.next); allPlaylists = [...allPlaylists, ...data.items]; playlistsData = data; } // 提取所有不重复的播放列表ID allPlaylists.forEach(playlist => { if (!PIDS.includes(playlist.id)) { PIDS.push(playlist.id); } }); // 遍历每个播放列表,获取所有歌曲ID for (const pid of PIDS) { let listData = await getPlaylistItems(pid); // 提取当前页歌曲ID listData.data.items.forEach(item => { const songID = item.track?.id; if (songID && !songIDs.includes(songID)) { songIDs.push(songID); } }); // 处理歌曲分页 while (listData.data.next) { listData = await axios.get(listData.data.next); listData.data.items.forEach(item => { const songID = item.track?.id; if (songID && !songIDs.includes(songID)) { songIDs.push(songID); } }); } } return songIDs; }
修改调用函数
export const MergePlaylists = async () => { const CurrentSongList = await getAllUserSongIDs(); console.log("Cur: " + CurrentSongList.length); // 在这里用CurrentSongList创建新播放列表 }
如果需要在React组件中使用该数据
import { getAllUserSongIDs } from './path-to-your-function'; function YourComponent() { const [songIDs, setSongIDs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchSongIDs = async () => { try { const ids = await getAllUserSongIDs(); setSongIDs(ids); } catch (err) { console.error('获取歌曲ID失败:', err); } finally { setLoading(false); } }; fetchSongIDs(); }, []); if (loading) return <div>加载中...</div>; return ( <div> {/* 在这里渲染songIDs相关内容 */} <p>共获取到{songIDs.length}首歌曲</p> </div> ); }
- 原代码中
Object.entries(playlists).forEach可以简化为playlists.forEach,因为playlists是数组,直接遍历元素即可,不需要用Object.entries。 - 原代码中
songIDs.push(listData.data.items)是错误的,这会把整个歌曲对象数组推入songIDs,而我们只需要ID,所以删除这行。 - 使用可选链
item.track?.id避免track为null时的报错。
内容的提问来源于stack exchange,提问作者jade

