cd ~

, with read , about 16min.

记录useEffect闭包陷阱问题

记一次开发问题:

我有一组件A ,如下

// ...... STATE
  const [startTime, setStartTime] = useState(0);// 时间 tag
  const [endTime, setEndTime] = useState(0);
// ...... useEffect
  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;
// ......
    const handleTimeupdate = ()=>{
      console.log('videoRef.current.currentTime',videoRef.current.currentTime, "startTime: ", startTime, "endTime:", endTime)
      if(videoRef.current && videoRef.current.currentTime > endTime){
        // playAtFromRangeStart()
      }
    }
    video.addEventListener("timeupdate", handleTimeupdate)
  }, []);
// ......


// ...... Change state somewhere.
  const handleChange = (/*.....*/,percentage: number) => {
// ......
  const newTime = /* ...... */
    /* ...... */ setStartTime(newTime) : setEndTime(newTime);
// ......
  };
copy success

我预期是,视频在播放的时候,会频繁的触发 timeupdate 时间, 然后用户操作的时候会频繁触发 handleChange 事件以更新 startTimeendTime, 我期望 可以在 timeupdate 的handler 中,获取到最新的值。 发现始终都是 0

这种情况, 是由 React 的闭包特性和状态更新的异步性质导致的。

  1. 闭包问题:
    当你创建 handleTimeupdate 函数时,它捕获了当时的 startTimeendTime 值。即使你后来通过 setStartTimesetEndTime 更新了这些状态,handleTimeupdate 函数仍然引用的是旧的值。

  2. 异步状态更新:
    React 的状态更新是异步的。当你调用 setStartTimesetEndTime 时,状态并不会立即更新。

解决这个问题有几种方法:

  1. 使用 useRef 来存储这些值:
const startTimeRef = useRef(0);
const endTimeRef = useRef(0);

// 更新时
const updateTimes = (start, end) => {
  startTimeRef.current = start;
  endTimeRef.current = end;
  setStartTime(start);
  setEndTime(end);
};

// 在 handleTimeupdate 中
const handleTimeupdate = () => {
  console.log('videoRef.current.currentTime', videoRef.current.currentTime, 
              "startTime: ", startTimeRef.current, 
              "endTime:", endTimeRef.current);
  // ...
};
copy success
  1. 使用函数式更新和 useCallback
const handleTimeupdate = useCallback(() => {
  console.log('videoRef.current.currentTime', videoRef.current.currentTime, 
              "startTime: ", startTime, 
              "endTime:", endTime);
  // ...
}, [startTime, endTime]);

useEffect(() => {
  // ... 其他代码 ...
  video.addEventListener("timeupdate", handleTimeupdate);
  return () => {
    video.removeEventListener("timeupdate", handleTimeupdate);
  };
}, [handleTimeupdate]);
copy success
  1. 直接在事件处理函数中访问最新的状态:
const handleTimeupdate = () => {
  console.log('videoRef.current.currentTime', videoRef.current.currentTime, 
              "startTime: ", startTimeRef.current, 
              "endTime:", endTimeRef.current);
  // 使用 getter 函数获取最新状态
  const currentStartTime = getStartTime();
  const currentEndTime = getEndTime();
  // ...
};
copy success
  1. 使用 useEffect 监听 startTimeendTime 的变化:
useEffect(() => {
  const handleTimeupdate = () => {
    console.log('videoRef.current.currentTime', videoRef.current.currentTime, 
                "startTime: ", startTime, 
                "endTime:", endTime);
    // ...
  };

  const video = videoRef.current;
  if (video) {
    video.addEventListener("timeupdate", handleTimeupdate);
    return () => video.removeEventListener("timeupdate", handleTimeupdate);
  }
}, [startTime, endTime]);
copy success

这些方法中,我个人推荐使用第一种(useRef)或第二种(useCallback)方法,因为它们既能解决问题,又不会导致过多的重渲染。

cd ~
GO BACK (Backspace)
BACK TO TOP (ESC)
COMMENTS (C)