React中hover时意外触发状态更新?Youtube API遭频繁调用
我在获取Youtube URL的useEffect外部加了console.log(youtubeURL),发现只要鼠标移入或移出父div,就会触发URL获取操作,导致大量Youtube API调用被浪费。
原本应该只有点击React图标(FaPlay)时才会调用handlePlay函数,进而设置selectedTrack状态——这部分功能是正常的,但现在出现了hover触发的异常问题。
以下是我的代码:
const [hovered, setHovered] = useState(false); const [selectedTrack, setSelectedTrack] = useState<{ artist: string, title: string } | null>(null) const [youtubeURL, setYoutubeURL] = useState<string>('') const handlePlay = (title: string, artist: string) => { setSelectedTrack({ title, artist }); console.log('clicked') }; useEffect(() => { const getSongURL = async () => { if (selectedTrack) try { const encodedTitle = encodeURI(selectedTrack.title.toLowerCase()); const encodedArtist = encodeURI(selectedTrack.artist.toLowerCase()); const songURL = await axios.get(`http://localhost:8080/api/v1/songdata/req_song/${encodedTitle}%20${encodedArtist}`); setYoutubeURL(songURL?.data?.id) } catch (error) { console.log("Error finding album details:", error); } } getSongURL(); }, [selectedTrack]) console.log(youtubeURL) return ( <> <ul className='list-none m-0 py-0 px-[35px] relative'> <li className='box-border'> <div className='flex justify-between items-center py-0 px-[15px]'> {/* Inner details of block*/} <div className={`group w-full flex justify-between box-border items-center rounded-lg hover:bg-[#4c426e] cursor-pointer self-stretch min-w-0 h-[50px] p-0 mb-2 relative`} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}> <div className={`w-full text-[12px] min-w-[35px] max-w-[35px] text-left ml-2 self-center shrink text-lg text-[#989898] relative`}> <div className={`absolute top-0 left-0 w-full h-full flex items-center justify-center transition-all ease-in-out duration-300`}> <span style={{ opacity: hovered ? 0 : 100 }} className={`absolute top-0 left-0 w-full h-full flex items-center justify-center transition-all ease-in-out duration-300`} > {number} </span> <span style={{ opacity: hovered ? 100 : 0 }} className={`absolute bottom-0 left-0 w-full h-full flex items-center justify-center transition-all ease-in-out duration-300`} > <FaPlay onClick={() => handlePlay(title, artist)}/> </span> </div> </div> </div> </li> </ul> </> ) }
我感觉onMouseEnter和onMouseLeave命令似乎对其产生了影响,但onClick事件本不该关联hover操作,为何仅hover就会持续调用handlePlay函数呢?
问题原因与解决方案
核心原因
你的代码里,FaPlay图标所在的span是绝对定位的,hover时会从透明变为可见。当鼠标hover父div时,这个span会覆盖原来显示{number}的span区域——如果鼠标在hover过程中轻微移动,或者元素切换透明度时触发了浏览器的点击误判,就会意外触发FaPlay的onClick事件,调用handlePlay修改selectedTrack,进而触发useEffect里的API调用。
另外,你用useState管理hovered状态,每次hover都会触发组件重新渲染,也增加了误触的概率。
解决方案
1. 用CSS替代state管理hover状态(推荐)
直接用Tailwind的group-hover类实现图标切换,完全不需要hovered状态和鼠标事件监听,从根源减少组件渲染和误触可能:
// 删掉hovered相关的state、onMouseEnter/onMouseLeave return ( <> <ul className='list-none m-0 py-0 px-[35px] relative'> <li className='box-border'> <div className='flex justify-between items-center py-0 px-[15px]'> {/* Inner details of block*/} <div className={`group w-full flex justify-between box-border items-center rounded-lg hover:bg-[#4c426e] cursor-pointer self-stretch min-w-0 h-[50px] p-0 mb-2 relative`}> <div className={`w-full text-[12px] min-w-[35px] max-w-[35px] text-left ml-2 self-center shrink text-lg text-[#989898] relative`}> <span className={`absolute top-0 left-0 w-full h-full flex items-center justify-center transition-all ease-in-out duration-300 group-hover:opacity-0`}> {number} </span> <span className={`absolute top-0 left-0 w-full h-full flex items-center justify-center transition-all ease-in-out duration-300 opacity-0 group-hover:opacity-100`}> <FaPlay onClick={() => handlePlay(title, artist)}/> </span> </div> </div> </li> </ul> </> )
2. 阻止点击事件冒泡,避免误触
如果不想修改hover逻辑,给FaPlay的点击事件添加阻止冒泡,避免父元素的事件干扰:
<FaPlay onClick={(e) => { e.stopPropagation(); handlePlay(title, artist); }}/>
3. 给API调用加防抖,减少资源浪费
如果担心还有其他误触场景,可以给API调用加防抖,确保短时间内只执行一次:
import { debounce } from 'lodash'; import { useCallback } from 'react'; // 组件内定义防抖函数 const getSongURL = useCallback(debounce(async (track) => { if (!track) return; try { const encodedTitle = encodeURI(track.title.toLowerCase()); const encodedArtist = encodeURI(track.artist.toLowerCase()); const songURL = await axios.get(`http://localhost:8080/api/v1/songdata/req_song/${encodedTitle}%20${encodedArtist}`); setYoutubeURL(songURL?.data?.id); } catch (error) { console.log("Error finding album details:", error); } }, 500), []); useEffect(() => { if (selectedTrack) { getSongURL(selectedTrack); } }, [selectedTrack, getSongURL]);
内容的提问来源于stack exchange,提问作者CakeoftheCup

