React搜索栏组件优化:解决渲染延迟与内存泄漏问题
React搜索组件优化方案:解决延迟、白屏与内存泄漏问题
问题根源拆解
- 请求频率过高:输入每个字符都会触发
useEffect执行API请求,短时间内大量请求导致页面卡顿甚至白屏 - 内存泄漏:异步请求未取消,组件卸载后请求仍在进行,完成后尝试更新已卸载组件的状态
- 逻辑错误:调用
searchActors时直接使用初始状态的newMovieSearch[1].SID,此时电影搜索结果还未返回,会触发Cannot read property 'SID' of undefined错误 - loading状态失效:
setSearch(false)在请求发起后立即执行,导致加载提示根本不会显示
具体优化步骤
1. 添加防抖(Debounce)
防抖可以让API请求在用户停止输入一段时间后才触发,避免频繁请求。这里手动实现简单防抖逻辑:
const Home: React.FC = () => { // ... 其他状态 const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 300); // 300ms内无输入才触发搜索 return () => clearTimeout(timer); }, [searchTerm]); }
后续所有API请求都基于debouncedSearchTerm触发,而非原始的searchTerm。
2. 使用AbortController取消异步请求
在组件卸载或搜索词变化时,终止未完成的请求,避免内存泄漏:
useEffect(() => { if (!debouncedSearchTerm) { setMovieSearch([]); setActorSearch([]); return; } const controller = new AbortController(); const signal = controller.signal; setSearch(true); // 先请求电影,再基于电影结果请求演员 searchMovies(debouncedSearchTerm, { signal }) .then(movieResults => { setMovieSearch(movieResults); // 确保有电影结果再请求演员 if (movieResults.length >= 2) { return searchActors(debouncedSearchTerm, movieResults[1].SID, { signal }); } return []; }) .then(actorResults => { setActorSearch(actorResults); }) .catch(err => { // 忽略取消请求的错误 if (err.name !== 'AbortError') console.error(err); }) .finally(() => { setSearch(false); }); // 清理函数:取消请求 return () => controller.abort(); }, [debouncedSearchTerm]);
注意需要修改searchMovies和searchActors函数,支持接收signal参数传递给fetch:
// 示例API函数修改 export const searchMovies = (term: string, { signal }: { signal: AbortSignal }) => { return fetch(`/api/movies?search=${term}`, { signal }) .then(res => res.json()) .then(data => data as MovieAttributes[]); }; export const searchActors = (term: string, sid: string, { signal }: { signal: AbortSignal }) => { return fetch(`/api/actors?search=${term}&sid=${sid}`, { signal }) .then(res => res.json()) .then(data => data as ActorAttributes[]); };
3. 修复异步依赖逻辑
之前的代码在电影请求还未返回时就尝试读取newMovieSearch[1].SID,这完全是错误的。必须等电影请求完成后,再根据返回的结果调用演员搜索,也就是上面代码中searchMovies的.then里处理演员请求的逻辑。
4. 优化列表渲染性能
不要在map里做过滤逻辑,提前在状态更新时处理,或者直接基于搜索结果渲染(因为API已经返回过滤后的结果):
// 替换原来的列表渲染代码 <div className="App"> <ul className="posts"> <input type="text" value={searchTerm} onChange={handleSearchTermChange} /> {movieSearch.map((movie) => ( <li key={movie.OBJECTID}> <h3>{movie.NAME}</h3> <p>{movie.DIRECTOR}</p> <p>{movie.DESCRIP}</p> </li> ))} </ul> </div>
如果API没有做过滤,也可以在searchMovies返回后直接过滤,避免每次渲染都执行toLowerCase()和includes():
searchMovies(debouncedSearchTerm, { signal }) .then(rawResults => { const filteredMovies = rawResults.filter(movie => movie.NAME.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ); setMovieSearch(filteredMovies); // ... 后续逻辑 })
5. 边界情况处理
- 当搜索词为空时,清空搜索结果
- 处理电影结果不足2条的情况,避免读取
movieResults[1]时出错
完整优化后代码示例
import { useEffect, useState } from 'react'; import { searchMovies, searchActors, MovieAttributes, ActorAttributes } from "@utils/atts" const Home: React.FC = () => { const [search, setSearch] = useState(false); const [movieSearch, setMovieSearch] = useState<MovieAttributes[]>([]); const [actorSearch, setActorSearch] = useState<ActorAttributes[]>([]); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); // 防抖处理搜索词 useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 300); return () => clearTimeout(timer); }, [searchTerm]); // 处理搜索请求 useEffect(() => { // 搜索词为空时清空结果 if (!debouncedSearchTerm) { setMovieSearch([]); setActorSearch([]); return; } const controller = new AbortController(); const signal = controller.signal; setSearch(true); searchMovies(debouncedSearchTerm, { signal }) .then(movieResults => { setMovieSearch(movieResults); // 确保有足够的电影结果再请求演员 if (movieResults.length >= 2) { return searchActors(debouncedSearchTerm, movieResults[1].SID, { signal }); } return []; }) .then(actorResults => { setActorSearch(actorResults); }) .catch(err => { if (err.name !== 'AbortError') { console.error('搜索出错:', err); } }) .finally(() => { setSearch(false); }); // 清理函数:取消未完成的请求 return () => controller.abort(); }, [debouncedSearchTerm]); const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => { setSearchTerm(event.target.value); } return ( <div> <input type="text" value={searchTerm} onChange={handleSearchTermChange} /> {search && <p>搜索中...</p>} {movieSearch.length > 0 && <p>找到 {movieSearch.length} 部电影</p>} {actorSearch.length > 0 && <p>找到 {actorSearch.length} 位演员</p>} <div className="App"> <ul className="posts"> {movieSearch.map((movie) => ( <li key={movie.OBJECTID}> <h3>{movie.NAME}</h3> <p>{movie.DIRECTOR}</p> <p>{movie.DESCRIP}</p> </li> ))} </ul> </div> </div> ); } export default Home;
内容的提问来源于stack exchange,提问作者peachy
相关产品推荐
相关产品推荐

