Vite+React中useEffect内Spotify API Post请求重复触发问题
Spotify API POST请求重复触发导致Token覆盖问题
我在Vite+React应用中集成Spotify API时,遇到POST请求被触发2-4次的问题,导致Token数据被持续覆盖引发错误。尝试过合并独立的useEffect、移除React严格模式(但导致应用崩溃)、改用异步请求等方法,均未解决问题。
相关代码如下:
const client_id = ""; // Your client id const client_secret = ""; // Your secret const redirect_uri = "http://localhost:3000/callback"; const [accessToken, setAccessToken] = useState(""); const [refreshToken, setRefreshToken] = useState(""); const [expiresIn, setExpiresIn] = useState(""); useEffect(() => { const authParams = { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: "Basic " + new Buffer.from(client_id + ":" + client_secret).toString("base64"), }, body: new URLSearchParams({ grant_type: "authorization_code", redirect_uri: redirect_uri, code: code, }), }; const refreshParams = { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: "Basic " + new Buffer.from(client_id + ":" + client_secret).toString("base64"), }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: client_id, }), }; fetch("https://accounts.spotify.com/api/token", authParams) .then((result) => result.json()) .then((data) => { console.log(data); if (!accessToken) { setAccessToken(data.access_token); } setRefreshToken(data.refresh_token); setExpiresIn(data.expires_in); if (!expiresIn) { fetch("https://accounts.spotify.com/api/token", refreshParams) .then((res) => res.json()) .then((data) => { setExpiresIn(data.expires_in); setAccessToken(data.access_token); }); } console.log(data.access_token); // if (accessToken) return console.log("access toke +> " + accessToken); // else window.history.pushState({}, null, "/"); }) .catch((err) => { console.log(err); res.sendStatus(400); }); }, []);
解决方法
1. 用请求控制器取消重复请求
React严格模式在开发环境会双重执行useEffect,加上组件可能重复渲染,会导致请求多次发送。使用AbortController可以在组件卸载或重复执行时取消未完成的请求,避免重复触发。
2. 修复逻辑依赖错误
refreshParams原本依赖refreshToken的初始空值,此时发送刷新请求必然失败,应在获取到有效refresh_token后再动态创建请求参数。- 用API返回的
expires_in判断是否需要刷新,而非依赖组件状态(状态更新是异步的,判断时可能还未完成更新)。
3. 标记请求执行状态
用useRef标记请求是否已经发起过,避免重复执行逻辑。
修改后的代码:
const client_id = ""; // Your client id const client_secret = ""; // Your secret const redirect_uri = "http://localhost:3000/callback"; const [accessToken, setAccessToken] = useState(""); const [refreshToken, setRefreshToken] = useState(""); const [expiresIn, setExpiresIn] = useState(""); const hasFetched = useRef(false); // 标记请求是否已执行 useEffect(() => { // 已执行过请求则直接返回 if (hasFetched.current) return; hasFetched.current = true; const controller = new AbortController(); const signal = controller.signal; // 预生成基础认证头,避免重复计算 const authHeader = "Basic " + new Buffer.from(client_id + ":" + client_secret).toString("base64"); // 授权码换Token const fetchAuthToken = async () => { try { const response = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: authHeader, }, body: new URLSearchParams({ grant_type: "authorization_code", redirect_uri: redirect_uri, code: code, }), signal, }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Token获取失败"); setAccessToken(data.access_token); setRefreshToken(data.refresh_token); setExpiresIn(data.expires_in); // 若返回的expires_in无效,立即刷新Token if (!data.expires_in && data.refresh_token) { await fetchRefreshToken(data.refresh_token); } } catch (err) { // 忽略请求被取消的错误 if (err.name !== "AbortError") console.error("Token请求错误:", err); } }; // 刷新Token函数 const fetchRefreshToken = async (refreshToken) => { try { const response = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: authHeader, }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: client_id, }), signal, }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Token刷新失败"); setExpiresIn(data.expires_in); setAccessToken(data.access_token); } catch (err) { if (err.name !== "AbortError") console.error("Token刷新错误:", err); } }; fetchAuthToken(); // 组件卸载或重新执行useEffect时取消请求 return () => controller.abort(); }, [code]); // 添加code为依赖,确保code变化时才重新发起请求
额外检查点
- 确认父组件是否存在重复渲染情况,导致当前组件多次挂载。
- 开发环境的严格模式双重执行是预期行为,生产环境会自动消失,无需强行移除。
内容的提问来源于stack exchange,提问作者sullivanstresser
相关产品推荐
相关产品推荐

