为什么我用于请求Spotify API的useEffect会返回429错误?
问题根因
- 无依赖数组的useEffect死循环:useEffect不传入依赖数组时,每次组件渲染完成都会重复执行,而每个useEffect内部的setState操作会触发组件重新渲染,形成无限调用链路,短时间内发起大量请求触发Spotify接口的429限流。
- 加依赖后无token报错:
- 组件挂载时token还未完成初始化(从localStorage读取、写入SpotifyApi实例是异步的),直接发起请求时api实例还没有绑定有效accessToken
- Context中定义的
refreshableCall没有用useCallback包裹,每次Provider重渲染都会生成新的函数实例,如果将其加入useEffect依赖会触发不必要的重复执行,同时没有在请求前判断token是否已准备完成。
修复方案
第一步:修复SpotifyContext.js的逻辑
- 用useCallback包裹
refreshableCall和refreshAccessToken,稳定函数引用 - 添加刷新锁避免多个并发请求重复触发token刷新
修改后的核心代码:
// SpotifyContext.js 内部Provider部分 import { useRef, useCallback } from 'react' export function SpotifyAuthContextProvider({ children }) { const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null)) const [isRefreshing, setIsRefreshing] = useState(false) const pendingRequests = useRef([]) const hasToken = tokenInfo !== null useEffect(() => { if (tokenInfo === null) return; spotifyApi.setCredentials({ accessToken: tokenInfo.accessToken, refreshToken: tokenInfo.refreshToken, }) setStoredJSON('myApp:spotify', tokenInfo) }, [tokenInfo]) // 用useCallback包裹refreshAccessToken const refreshAccessToken = useCallback(async () => { if (isRefreshing) { // 已经在刷新中,把请求加入队列等待 return new Promise(resolve => { pendingRequests.current.push(resolve) }) } setIsRefreshing(true) try { const refreshToken = tokenInfo.refreshToken; const res = await axios.post("http://localhost:3001/refresh", { refreshToken }) const refreshedTokenInfo = { accessToken: res.data.accessToken, refreshToken: res.data.refreshToken || tokenInfo.refreshToken, expiresAt: Date.now() + (res.data.expiresIn * 1000) } setTokenInfo(refreshedTokenInfo) spotifyApi.setCredentials({ accessToken: refreshedTokenInfo.accessToken, refreshToken: refreshedTokenInfo.refreshToken, }) // 执行所有等待中的请求 pendingRequests.current.forEach(resolve => resolve(refreshedTokenInfo)) pendingRequests.current = [] return refreshedTokenInfo } finally { setIsRefreshing(false) } }, [tokenInfo, isRefreshing]) // 用useCallback包裹refreshableCall const refreshableCall = useCallback(async (callApiFunc) => { if (!tokenInfo) throw new Error("No token available") if (Date.now() > tokenInfo.expiresAt) await refreshAccessToken(); try { return await callApiFunc() } catch (err) { if (err.name !== "WebapiAuthenticationError") throw err; } return refreshAccessToken().then(callApiFunc) }, [tokenInfo, refreshAccessToken]) // 其余exchangeCode逻辑不变 const exchangeCode = useCallback((code) => { return axios .post("http://localhost:3001/login", { code }) .then(res => { const { accessToken, refreshToken, expiresIn } = res.data; setTokenInfo({ accessToken, refreshToken, expiresAt: Date.now() + (expiresIn * 1000) }); }) }, []) return ( <SpotifyAuthContext.Provider value={{ api: spotifyApi, exchangeCode, hasToken, refreshableCall, refreshAccessToken }}> {children} </SpotifyAuthContext.Provider> ) }
第二步:修复Profile.js的useEffect逻辑
- 给useEffect添加依赖数组,并且只在hasToken为true时发起请求
- 合并多个请求到同一个useEffect减少冗余代码
修改后的核心代码:
// Profile.js const { api, refreshableCall, hasToken } = useSpotify() const [error, setError] = useState(null) const [userName, setUserName] = useState("") const [userFollowers, setUserFollowers] = useState("") const [userImage, setUserImage] = useState([]) const [userLink, setUserLink] = useState("") const [userId, setUserId] = useState("") const [userFollowing, setUserFollowing] = useState("") const [userTopArtists, setUserTopArtists] = useState([]) const [userTopSongs, setUserTopSongs] = useState([]) useEffect(() => { if (!hasToken) return // token未准备好直接返回 let disposed = false // 并行发起所有请求 Promise.all([ refreshableCall(() => api.getMyTopTracks({ limit: 10, time_range: "long_term" })), refreshableCall(() => api.getMe()), refreshableCall(() => api.getFollowedArtists()), refreshableCall(() => api.getMyTopArtists({ limit: 10, time_range: "long_term" })) ]).then(([topTracksRes, meRes, followedRes, topArtistsRes]) => { if (disposed) return setUserTopSongs(topTracksRes.body.items) const meData = meRes.body setUserName(meData.display_name) setUserImage(meData.images) setUserFollowers(meData.followers["total"]) setUserLink(meData.external_urls.spotify) setUserId(meData.id) setUserFollowing(followedRes.body.artists.total) setUserTopArtists(topArtistsRes.body.items) setError(null) }).catch(err => { if (disposed) return setError(err) // 重置状态 setUserTopSongs([]) setUserName("") setUserImage([]) setUserFollowers("") setUserLink("") setUserId("") setUserFollowing([]) setUserTopArtists([]) }) return () => disposed = true }, [refreshableCall, hasToken]) // 只在refreshableCall或者hasToken变化时执行
额外优化建议
如果后续还遇到429错误,可以给Spotify请求添加简单的限流逻辑,或者对重复的请求做缓存,避免相同接口重复调用。
内容的提问来源于stack exchange,提问作者Amogh
相关产品推荐
相关产品推荐

