React应用初始渲染后持续调用YouTube API问题排查
React应用持续重复调用YouTube API的问题
问题现象
开发的React应用在初始渲染完成后,仍持续向YouTube API发送大量请求,即使已经实现了localStorage缓存机制,网络面板仍能看到重复请求,严重消耗API配额。问题出在实现获取8个特定YouTube视频的自定义hook useVideos之后。
相关代码
自定义hook useVideos
import { useState, useEffect } from "react"; import youtube from "../apis/youtube"; const useVideos = () => { const [videos, setVideos] = useState(() => { const cachedVideos = localStorage.getItem("cachedVideos"); return cachedVideos ? JSON.parse(cachedVideos) : []; }); useEffect(() => { if (videos.length === 0) { fetchSpecificVideos(); } }, []); const fetchSpecificVideos = async () => { try { const videoIds = [ "VIDEO_1", "VIDEO_2", "VIDEO_3", "VIDEO_4", "VIDEO_5", "VIDEO_6", "VIDEO_7", "VIDEO_8", ]; const videoData = []; for (const videoId of videoIds) { const response = await youtube.get("/videos", { params: { id: videoId, part: "snippet", maxResults: 8, }, }); if (response.data.items && response.data.items.length > 0) { videoData.push(response.data.items[0]); } } setVideos(videoData); localStorage.setItem("cachedVideos", JSON.stringify(videoData)); } catch (error) { console.error("Error fetching videos:", error); } }; return [videos, fetchSpecificVideos]; }; export default useVideos;
YouTube API配置文件
import axios from "axios"; const KEY = "my-youtube-api-key"; export default axios.create({ baseURL: "https://www.googleapis.com/youtube/v3", params: { part: "snippet", maxResults: 8, key: KEY, }, });
App组件代码
import React, { useState, useEffect } from "react"; import Header from "./Header"; import VideoList from "./VideoList"; import VideoDetail from "./VideoDetail"; import useVideos from "../hooks/useVideos"; const App = () => { const [selectedVideo, setSelectedVideo] = useState(null); const [videos, fetchSpecificVideos] = useVideos(); useEffect(() => { if (videos.length === 0) { fetchSpecificVideos(); } }, [videos, fetchSpecificVideos]); useEffect(() => { if (videos.length > 0) { setSelectedVideo(videos[0]); } }, [videos]); return ( <div className="ui container"> <div className="ui inverted menu"> <Header /> </div> <VideoList onVideoSelect={setSelectedVideo} videos={videos} selectedVideo={selectedVideo} /> <VideoDetail video={selectedVideo} /> </div> ); }; export default App;
排查情况
- 仅在App父组件中使用
useVideos,未在其他组件中调用 - 即使启用localStorage缓存,仍会出现大量重复请求
- 怀疑需要通过记忆化(memoization)解决
解决方案
问题根源
fetchSpecificVideos函数重复创建:useVideos中每次组件渲染时,fetchSpecificVideos都会被重新定义,导致App组件中依赖该函数的useEffect每次都触发,进而重复调用请求。- 双重触发请求:
useVideos内部的useEffect已经在初始化时检查并请求数据,App组件中又添加了相同逻辑的useEffect,导致重复触发。
修复步骤
- 记忆化请求函数:用
useCallback包裹fetchSpecificVideos,避免每次渲染重新创建函数。 - 移除重复请求逻辑:保留
useVideos内部的初始化请求逻辑,App组件中无需重复判断调用。 - 添加请求锁:防止同一时间多次触发请求,避免并发调用API。
- 优化请求方式:将串行请求改为并行,减少总请求时间。
修改后的useVideos代码
import { useState, useEffect, useCallback } from "react"; import youtube from "../apis/youtube"; const useVideos = () => { const [videos, setVideos] = useState(() => { const cachedVideos = localStorage.getItem("cachedVideos"); return cachedVideos ? JSON.parse(cachedVideos) : []; }); const [isFetching, setIsFetching] = useState(false); const fetchSpecificVideos = useCallback(async () => { if (isFetching || videos.length > 0) return; setIsFetching(true); try { const videoIds = [ "VIDEO_1", "VIDEO_2", "VIDEO_3", "VIDEO_4", "VIDEO_5", "VIDEO_6", "VIDEO_7", "VIDEO_8", ]; // 并行请求优化 const requests = videoIds.map(id => youtube.get("/videos", { params: { id: id, part: "snippet", maxResults: 1, }, }) ); const responses = await Promise.all(requests); const videoData = responses.reduce((acc, response) => { if (response.data.items?.length) acc.push(response.data.items[0]); return acc; }, []); setVideos(videoData); localStorage.setItem("cachedVideos", JSON.stringify(videoData)); } catch (error) { console.error("Error fetching videos:", error); } finally { setIsFetching(false); } }, [videos, isFetching]); useEffect(() => { fetchSpecificVideos(); }, [fetchSpecificVideos]); return [videos, fetchSpecificVideos]; }; export default useVideos;
修改后的App组件代码
import React, { useState, useEffect } from "react"; import Header from "./Header"; import VideoList from "./VideoList"; import VideoDetail from "./VideoDetail"; import useVideos from "../hooks/useVideos"; const App = () => { const [selectedVideo, setSelectedVideo] = useState(null); const [videos] = useVideos(); useEffect(() => { if (videos.length > 0) { setSelectedVideo(videos[0]); } }, [videos]); return ( <div className="ui container"> <div className="ui inverted menu"> <Header /> </div> <VideoList onVideoSelect={setSelectedVideo} videos={videos} selectedVideo={selectedVideo} /> <VideoDetail video={selectedVideo} /> </div> ); }; export default App;
内容的提问来源于stack exchange,提问作者Daniel
相关产品推荐
相关产品推荐

