React Hook竞态条件问题:如何确保缩略图缓存加载完成再渲染?
问题描述
我正在完成首个React应用的收尾工作,却遇到了棘手问题:需要为展示条目缓存缩略图,于是用了如下useEffect实现:
var thumbnailLookUp = {}; useEffect(() => { fetch("http://localhost:3003/thumbnailPreload") .then((response) => response.json()) .then((data) => { for (var row of data) { thumbnailLookUp[row.uuid] = row.thumb; } }) .then(countClips()) .catch((err) => { console.log(err.message); }) }, []);
countClips函数会组装主渲染中通过map()渲染的数据列表,其中包含@mui的自定义组件CardMedia:
<CardMedia component="img" height="100" src={`https://d3pe68qfxlhu0a.cloudfront.net/${ thumbnailLookUp[event.uuid] }`} />
问题在于,页面完全重载时,半数情况正常,半数情况所有thumbnailLookUp[event.uuid]结果均为undefined,且结果完全一致。这说明useEffect与渲染间存在竞态条件,设置断点可确保每次成功。
请问如何优化代码,确保thumbnailLookUp总能在渲染前(或必要时触发二次渲染)完成填充?
解决方案
核心问题在于你用普通变量thumbnailLookUp存储缓存数据,React无法追踪它的变化,导致数据更新后不会触发重新渲染;同时then(countClips())的写法是直接执行函数,而非传递回调,会导致countClips在数据未加载完成时就执行,进一步加剧竞态问题。
1. 用React状态管理缓存数据
将thumbnailLookUp改为React状态,这样数据更新时会自动触发组件重新渲染:
import { useState, useEffect } from 'react'; // 初始化空对象作为缓存 const [thumbnailLookUp, setThumbnailLookUp] = useState({});
2. 修正useEffect中的异步逻辑
- 把
then(countClips())改为then(() => countClips()),确保数据加载完成后再执行countClips - 更新状态时创建新对象,避免直接修改原对象:
useEffect(() => { fetch("http://localhost:3003/thumbnailPreload") .then((response) => response.json()) .then((data) => { const newLookup = {}; for (const row of data) { newLookup[row.uuid] = row.thumb; } setThumbnailLookUp(newLookup); }) .then(() => countClips()) // 确保数据加载完成后再执行组装逻辑 .catch((err) => { console.log(err.message); }) }, []);
3. 处理渲染时的加载状态
如果countClips依赖thumbnailLookUp的数据,建议在数据未加载完成时显示加载状态,避免出现undefined的图片地址:
const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); // 修改countClips,让它更新事件列表状态 const countClips = () => { // 替换为你原有的组装逻辑,使用最新的thumbnailLookUp const processedEvents = /* 你的数据组装代码 */; setEvents(processedEvents); setIsLoading(false); }; // 渲染前判断加载状态 if (isLoading) { return <div>加载中...</div>; } return ( // 你的渲染容器 {events.map(event => ( <CardMedia component="img" height="100" src={`https://d3pe68qfxlhu0a.cloudfront.net/${ thumbnailLookUp[event.uuid] || '' }`} alt={event.name || '缩略图'} // 可选:添加加载失败占位图 onError={(e) => { e.target.src = '你的占位图地址'; }} /> ))} );
额外优化:避免内存泄漏(可选)
如果组件可能在请求完成前卸载,用AbortController取消请求,防止内存泄漏:
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("http://localhost:3003/thumbnailPreload", { signal }) .then((response) => response.json()) .then((data) => { const newLookup = {}; for (const row of data) { newLookup[row.uuid] = row.thumb; } setThumbnailLookUp(newLookup); }) .then(() => countClips()) .catch((err) => { if (err.name !== 'AbortError') { console.log(err.message); } }); // 组件卸载时取消请求 return () => controller.abort(); }, []);
这样修改后,thumbnailLookUp的变化会触发组件重新渲染,确保渲染时总能拿到最新的缓存数据,彻底解决竞态问题。
内容的提问来源于stack exchange,提问作者Joymaker
相关产品推荐
相关产品推荐

