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

React+Vite中BrowserRouter引发HTTP请求重复触发问题求助

React HTTP请求重复触发问题排查与解决

问题描述

并非React专家,当前遇到HTTP请求重复触发的问题,通过React Profiler发现是BrowserRouter发起了多次调用,不清楚为何会出现这类重复请求。后端使用Django,相关HTTP请求日志、app.tsx及ViewDashboard.tsx代码如下:

HTTP请求日志

System check identified no issues (0 silenced).
August 29, 2023 - 10:57:35
Django version 4.2.3, using settings 'REDACTED.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

[29/Aug/2023 10:57:37] "OPTIONS /users/api/v1/token/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "POST /users/api/v1/token/ HTTP/1.1" 200 483
[29/Aug/2023 10:57:38] "OPTIONS /users/api/v1/token/refresh/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "POST /users/api/v1/token/refresh/ HTTP/1.1" 200 483
[29/Aug/2023 10:57:38] "OPTIONS /users/api/v1/token/refresh/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "POST /users/api/v1/token/refresh/ HTTP/1.1" 200 483
[29/Aug/2023 10:57:38] "OPTIONS /dashboards/api/v1/dashboards/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "OPTIONS /dashboards/api/v1/dashboards/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "GET /dashboards/api/v1/dashboards/ HTTP/1.1" 200 146
[29/Aug/2023 10:57:38] "OPTIONS /users/api/v1/token/refresh/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "OPTIONS /users/api/v1/token/refresh/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "POST /users/api/v1/token/refresh/ HTTP/1.1" 200 483
[29/Aug/2023 10:57:38] "POST /users/api/v1/token/refresh/ HTTP/1.1" 200 483
[29/Aug/2023 10:57:38] "OPTIONS /dashboards/api/v1/embed/REDACTED/REDACTED/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:38] "OPTIONS /dashboards/api/v1/embed/REDACTED/REDACTED/ HTTP/1.1" 200 0
[29/Aug/2023 10:57:39] "GET /dashboards/api/v1/dashboards/ HTTP/1.1" 200 146
[29/Aug/2023 10:57:40] "GET /dashboards/api/v1/embed/REDACTED/REDACTED/ HTTP/1.1" 200 2555
[29/Aug/2023 10:57:40] "GET /dashboards/api/v1/embed/REDACTED/REDACTED/ HTTP/1.1" 200 2555

app.tsx代码

import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import './App.css';
import Login from './Login';
import SelectDashboard from './SelectDashboard';
import ViewDashboard from './ViewDashboard';

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Login />} />
        <Route path="/select-dashboard" element={<SelectDashboard />} />
        <Route path="/dashboard" element={<ViewDashboard />} />
        <Route
          path="*"
          element={<Navigate to="/" replace />}
        />
      </Routes>
    </Router>
  );
};

export default App;

ViewDashboard.tsx代码

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import * as pbi from 'powerbi-client';
import './App.css';

const ViewDashboard: React.FC = () => {
  const navigate = useNavigate();
  const [embedData, setEmbedData] = useState<any | null>(null);

  const handleLogout = () => {
    // Clear localStorage items
    localStorage.removeItem('refresh');
    localStorage.removeItem('selectedWorkspaceId');
    localStorage.removeItem('selectedReportId');

    navigate('/login');
  };

  const getEmbed = async () => {
    // Try to get the refresh token from local storage
    try {
      const refresh = localStorage.getItem('refresh') || '';
      const refreshRequestBody = JSON.stringify({ refresh });

      // Get a new access token
      const newTokenResponse = await fetch('http://localhost:8000/users/api/v1/token/refresh/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: refreshRequestBody,
      });

      const newToken = await newTokenResponse.json();
      localStorage.setItem('jwt', newToken.access);

      const response = await fetch(
        'http://localhost:8000/dashboards/api/v1/embed/' +
          localStorage.getItem('selectedWorkspaceId') +
          '/' +
          localStorage.getItem('selectedReportId') +
          '/',
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            Authorization: 'Bearer ' + localStorage.getItem('jwt'),
          },
        }
      );
      const data = await response.json();
      return data;
    } catch (error) {
      redirect('/login');
    }
  };

  const redirect = (path: string) => {
    navigate(path);
  };

  const embedDashboard = async () => {
    try {
      const data = await getEmbed();
      setEmbedData(data);
    } catch (error) {
      throw error;
    }
  };

  useEffect(() => {
    embedDashboard();
  }, []); // Only run once when the component mounts

  useEffect(() => {
    if (embedData) {
      // Embed the Power BI dashboard here
      const config: pbi.IEmbedConfiguration = {
        type: 'report',
        id: embedData.reports[0].id,
        embedUrl: embedData.reports[0].embedUrl,
        accessToken: embedData.token,
        tokenType: pbi.models.TokenType.Embed,
      };

      const embedContainer = document.querySelector('.report-div');
      if (embedContainer) {
        // @ts-ignore
        const dashboard = powerbi.embed(embedContainer, config);
        dashboard.off('loaded'); // Prevent any additional event listeners
      }
    }
  }, [embedData]);

  return (
    <>
      <div className="horizontal-bar">
        <button onClick={handleLogout} className="logout-button">
          Logout
        </button>
      </div>
      <div className="dashboard-container">
        <div className="report-div"></div>
      </div>
    </>
  );
};

export default ViewDashboard;

问题原因与解决方案

核心原因

  1. React StrictMode 双重调用:开发环境下,StrictMode会主动双重调用useEffect、useState初始化等方法,目的是检测潜在的副作用问题。你的初始化请求正好在这个范围内,所以被触发两次。
  2. 组件重复挂载:检查是否存在路由配置错误,导致ViewDashboard被多次渲染;或者SelectDashboard中跳转逻辑有重复触发的情况。

解决步骤

  1. 验证StrictMode影响:打开index.tsx,如果存在<React.StrictMode>包裹<App />,暂时注释后重新运行项目。如果请求不再重复,说明是StrictMode的开发模式特性,生产环境会自动恢复正常,无需修改代码。
  2. 添加请求取消机制:即使是开发模式,也可以通过AbortController避免无效的重复请求,修改ViewDashboard.tsx代码如下:
// 修改useEffect,添加AbortController
useEffect(() => {
  const abortController = new AbortController();
  
  const embedDashboard = async () => {
    try {
      const data = await getEmbed(abortController.signal);
      setEmbedData(data);
    } catch (error) {
      if ((error as Error).name !== 'AbortError') {
        redirect('/login');
      }
    }
  };

  embedDashboard();

  return () => abortController.abort(); // 组件卸载或重复执行时取消请求
}, []);

// 修改getEmbed函数,接收signal参数
const getEmbed = async (signal: AbortSignal) => {
  try {
    const refresh = localStorage.getItem('refresh') || '';
    const refreshRequestBody = JSON.stringify({ refresh });

    const newTokenResponse = await fetch('http://localhost:8000/users/api/v1/token/refresh/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: refreshRequestBody,
      signal, // 传入取消信号
    });

    const newToken = await newTokenResponse.json();
    localStorage.setItem('jwt', newToken.access);

    const response = await fetch(
      'http://localhost:8000/dashboards/api/v1/embed/' +
        localStorage.getItem('selectedWorkspaceId') +
        '/' +
        localStorage.getItem('selectedReportId') +
        '/',
      {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          Authorization: 'Bearer ' + localStorage.getItem('jwt'),
        },
        signal, // 传入取消信号
      }
    );
    const data = await response.json();
    return data;
  } catch (error) {
    if ((error as Error).name !== 'AbortError') {
      redirect('/login');
    }
    throw error;
  }
};
  1. 检查路由跳转逻辑:确认SelectDashboard组件中跳转到/dashboard的代码是否只执行一次,比如是否存在点击事件重复绑定、状态更新导致重复跳转的情况。

内容的提问来源于stack exchange,提问作者holyshilo111

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 04:00:57