You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 20:22:35