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

React useEffect 跳过第二次渲染问题排查与修复求助

问题原因

你的代码核心问题是useEffect依赖了screenResponse,每次调用setScreenResponse更新状态后,useEffect都会重新执行。而每次重新执行时,只要index.current还没到阈值,就会创建一个新的setInterval,导致多个定时器同时运行:

  • 第一个定时器执行tick,把index从0加到1,更新screenResponse为"L";
  • 此时useEffect重新触发,又创建了第二个定时器;
  • 两个定时器几乎同时执行下一次tick,index直接从1跳到2,跳过了字符"o",直接添加"r",所以你看不到"Lo"的状态。

另外,console.log(screenResponse)打印的是旧状态值,因为状态更新是异步的,screenResponse在tick函数里是闭包捕获的旧值,这也会让你误以为中间状态没出现。

修复方案

重构代码,让定时器只初始化一次,同时单独处理skipDialogue的触发逻辑:

import { useState, useEffect, useRef } from 'react';

function App() {
  const startingLine = `Lorem ipsum.`;
  const [screenResponse, setScreenResponse] = useState('');
  const [skipDialogue, setSkipDialogue] = useState(false);
  const index = useRef(0);
  const intervalRef = useRef(null);

  // 处理打字定时器
  useEffect(() => {
    // 跳过对话时直接显示完整文本并清除定时器
    if (skipDialogue) {
      setScreenResponse(startingLine);
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
      return;
    }

    // 初始化定时器,只执行一次
    intervalRef.current = setInterval(() => {
      if (index.current < startingLine.length) {
        setScreenResponse(prev => prev + startingLine[index.current]);
        index.current++;
      } else {
        clearInterval(intervalRef.current);
      }
    }, 500);

    // 组件卸载时清除定时器
    return () => clearInterval(intervalRef.current);
  }, [skipDialogue, startingLine]);

  // 重置逻辑(如果需要重新触发打字效果)
  const resetDialogue = () => {
    setScreenResponse('');
    index.current = 0;
    setSkipDialogue(false);
  };

  return (
    <div>
      <p>{screenResponse}</p>
      <button onClick={() => setSkipDialogue(true)}>跳过对话</button>
      <button onClick={resetDialogue}>重新播放</button>
    </div>
  );
}

export default App;

关键优化点

  • 去掉screenResponse作为useEffect的依赖,改用空依赖数组配合useRef保存定时器引用,确保只创建一个定时器;
  • 用intervalRef保存定时器ID,方便在跳过对话或组件卸载时清除;
  • 直接判断index.current < startingLine.length即可,不需要减1——因为当index等于字符串长度时,循环停止,不会出现undefined;
  • 单独监听skipDialogue状态,触发时直接设置完整文本并清除定时器。
额外说明

之前的console.log(screenResponse)无法正确打印实时状态,因为状态更新是异步的,闭包会捕获旧的状态值。如果需要调试,可以在setScreenResponse的回调里打印,或者用useEffect监听screenResponse的变化:

useEffect(() => {
  console.log('当前显示:', screenResponse);
}, [screenResponse]);

内容的提问来源于stack exchange,提问作者theLoganaut

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 09:40:31