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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 17:02:07