如何解决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
相关产品推荐
相关产品推荐

