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

如何解决ReactJS中下拉菜单触发重复API请求的问题

解决下拉菜单频繁触发远程API请求的问题

问题根源

你的代码里,API请求逻辑和下拉菜单的状态监听被放在同一个useEffect中,并且这个useEffect的依赖是openDropdownId。每次点击打开/关闭下拉菜单时,openDropdownId状态都会变化,导致useEffect重新执行,进而重复调用远程API,对数据库造成不必要的性能消耗。

解决方案

根据实际业务场景,有两种优化方式:

方式一:仅组件初始化时请求一次数据(通用下拉内容场景)

如果下拉菜单的内容不需要针对每条记录单独获取,或是全局通用的数据,只需在组件挂载时请求一次API即可。把API请求和下拉的UI逻辑拆分成两个独立的useEffect:

import React, { useState, useEffect, useRef } from 'react';
import './App.css';

const App = () => {
  const recData = [
    { id: '106781', title: 'first title', name: 'jon' },
    { id: '199702', title: 'second title', name: 'mike' },
    { id: '106673', title: 'third title', name: 'Ann' },
  ];

  const [openDropdownId, setOpenDropdownId] = useState(null);
  const openDropdownContainerRef = useRef(null);
  const toggleDropdown = (id) => {
    setOpenDropdownId((prevOpenId) => (prevOpenId === id ? null : id));
  };

  // 仅在组件挂载时请求一次远程数据
  useEffect(() => {
    const fetchRemoteData = async () => {
      // API Call goes here
      alert('Data Fetched');
    };
    fetchRemoteData();
  }, []); // 空依赖数组,确保只执行一次

  // 单独处理点击外部关闭下拉的逻辑
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (
        openDropdownId &&
        openDropdownContainerRef.current &&
        !openDropdownContainerRef.current.contains(event.target)
      ) {
        setOpenDropdownId(null);
      }
    };

    if (openDropdownId) {
      document.addEventListener('mousedown', handleClickOutside);
    }

    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [openDropdownId]);

  return (
    <div>
      <h1>Welcome....</h1>

      {recData.map((rec) => {
        const url_id = rec.id;
        const isCurrentDropdownOpen = openDropdownId === rec.id;

        return (
          <div key={rec.id}>
            <div>Title: {rec.title}</div>
            <div>Name: {rec.name}</div>

            <p>
              <div
                className='dropdown'
                ref={isCurrentDropdownOpen ? openDropdownContainerRef : null}
              >
                <button
                  onClick={() => toggleDropdown(rec.id)}
                  className='dropdown-button'
                >
                  Menu{' '}
                  <span
                    className={`arrow ${isCurrentDropdownOpen ? 'open' : ''}`}
                  >
                    ▾
                  </span>
                </button>
                {isCurrentDropdownOpen && (
                  <div className='dropdown-content'>
                    (Url ID: {url_id})
                    <a
                      href={`https://www.example.com/${url_id}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 1
                    </a>
                    <a
                      href={`https://www.example.com/${rec.title}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 2
                    </a>
                    <a
                      href={`https://www.example.com/${rec.name}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 3
                    </a>
                  </div>
                )}
              </div>
            </p>
          </div>
        );
      })}
    </div>
  );
};

export default App;

方式二:按需请求+缓存(每条记录需单独获取下拉内容场景)

如果每条记录的下拉菜单需要加载对应专属的远程数据,那就通过缓存已请求过的数据,避免重复请求同一记录的内容:

import React, { useState, useEffect, useRef } from 'react';
import './App.css';

const App = () => {
  const recData = [
    { id: '106781', title: 'first title', name: 'jon' },
    { id: '199702', title: 'second title', name: 'mike' },
    { id: '106673', title: 'third title', name: 'Ann' },
  ];

  const [openDropdownId, setOpenDropdownId] = useState(null);
  // 用对象缓存每条记录的下拉数据,key为记录id
  const [dropdownDataCache, setDropdownDataCache] = useState({});
  const openDropdownContainerRef = useRef(null);

  const toggleDropdown = (id) => {
    setOpenDropdownId((prevOpenId) => (prevOpenId === id ? null : id));
  };

  // 仅当打开新的下拉菜单且未缓存数据时,才请求API
  useEffect(() => {
    if (!openDropdownId) return;

    // 检查缓存,已有数据则直接跳过请求
    if (dropdownDataCache[openDropdownId]) {
      return;
    }

    const fetchRemoteData = async () => {
      // 实际API请求,替换成你的接口地址
      // const res = await fetch(`/api/dropdown-data/${openDropdownId}`);
      // const data = await res.json();
      const data = {}; // 模拟接口返回数据
      alert(`Data Fetched for record ${openDropdownId}`);
      // 更新缓存
      setDropdownDataCache(prev => ({ ...prev, [openDropdownId]: data }));
    };

    fetchRemoteData();
  }, [openDropdownId, dropdownDataCache]);

  // 点击外部关闭下拉的逻辑保持不变
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (
        openDropdownId &&
        openDropdownContainerRef.current &&
        !openDropdownContainerRef.current.contains(event.target)
      ) {
        setOpenDropdownId(null);
      }
    };

    if (openDropdownId) {
      document.addEventListener('mousedown', handleClickOutside);
    }

    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [openDropdownId]);

  return (
    <div>
      <h1>Welcome....</h1>

      {recData.map((rec) => {
        const url_id = rec.id;
        const isCurrentDropdownOpen = openDropdownId === rec.id;
        // 从缓存中获取当前记录的下拉数据
        const currentDropdownData = dropdownDataCache[rec.id];

        return (
          <div key={rec.id}>
            <div>Title: {rec.title}</div>
            <div>Name: {rec.name}</div>

            <p>
              <div
                className='dropdown'
                ref={isCurrentDropdownOpen ? openDropdownContainerRef : null}
              >
                <button
                  onClick={() => toggleDropdown(rec.id)}
                  className='dropdown-button'
                >
                  Menu{' '}
                  <span
                    className={`arrow ${isCurrentDropdownOpen ? 'open' : ''}`}
                  >
                    ▾
                  </span>
                </button>
                {isCurrentDropdownOpen && (
                  <div className='dropdown-content'>
                    (Url ID: {url_id})
                    {/* 如果是动态数据,这里用currentDropdownData渲染 */}
                    <a
                      href={`https://www.example.com/${url_id}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 1
                    </a>
                    <a
                      href={`https://www.example.com/${rec.title}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 2
                    </a>
                    <a
                      href={`https://www.example.com/${rec.name}`}
                      target='_blank'
                      rel='noopener noreferrer'
                    >
                      Link 3
                    </a>
                  </div>
                )}
              </div>
            </p>
          </div>
        );
      })}
    </div>
  );
};

export default App;

核心思路

把API请求逻辑和下拉菜单的UI状态逻辑解耦,严格控制API请求的触发时机:要么只在组件初始化时请求一次,要么仅在需要加载新数据且无缓存时才请求,避免因下拉状态变化导致的无意义重复请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:20:01