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

React中useEffect与状态更新配合实现Glitch动画的问题排查

问题

我正在开发一个个人项目,父组件<PageName />包含子组件<GlitchingComponent />,后者通过children props接收渲染内容,示例用法为<GlitchingComponent children={"The body content of this page"} />。

<GlitchingComponent>需在挂载时执行以下步骤:

  1. 生成10至36之间的随机数
  2. 将随机数转换为36进制字符串
  3. 截取单个字符
  4. 基于该字符生成正则表达式
  5. 遍历children的每个元素,执行regexp.test()
  6. 匹配成功的元素替换为带样式类的<span>,否则返回原元素

这些步骤仅需在组件挂载时执行,因此我使用空依赖的useEffect初始化content状态:

React.useEffect(
    () => {
      //content is a state variable of GlitchingComponent
      setContent(generateNewBody());
    },
    []
);

但<span>的样式依赖组件的glitching状态,需要内容重新渲染但不重复调用generateNewBody()(否则会重复生成随机数,破坏动画)。当前代码实现如下:

//GlitchingComponent.js
import * as React from "react";

export function GlitchingComponent({ children }) {
  const [glitching, setGlitching] = React.useState(0),
    [triggerState, setTriggerState] = React.useState(0),
    [content, setContent] = React.useState(null);

  const generateApplicationChars = (characters = 2) => {
    let output = [];
    for (let i = 0; i < characters; i++) {
      output.push(Math.floor(Math.random() * (36 - 10) + 10).toString(36));
    }
    return output;
  };

//Steps 1-6 outlined above
  const generateNewBody = () => {
    const mainContent = children;
    const mainContentArray = mainContent.split(" ");
    const regExps = generateApplicationChars(3).map((e) => {
      return new RegExp(e, "i");
    });
    const newBody = mainContentArray.map((e) => {
      if (regExps.some((rexp) => rexp.test(e))) {
        return (
          <span
            className={`toGlitch${
              glitching == 0 ? "" : " glitching-" + glitching
            }`}
          >
            {e + " "}
          </span>
        );
      } else {
        return e + " ";
      }
    });
    return newBody;
  };

//The state update that controls the styles
  const glitchController = () => {
    setTriggerState(triggerState + 1);
    setGlitching(1);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(0);
    }, 133);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(1);
    }, 220);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(2);
    }, 350);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(3);
    }, 600);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(0);
    }, 750);
    setTimeout(() => {
      setTriggerState(triggerState + 1);
      setGlitching(3);
    }, 800);
    setTimeout(() => {
      setGlitching(0);
      setTriggerState(0);
    }, 1000);
  };

//The useEffect hook to generate the new content on component mount
  React.useEffect(
    () => {
      setContent(generateNewBody());
    },
    []
  );
//An attempt to call glitchController() on triggerState update
  React.useEffect(() => {
    glitchController();
  }, [triggerState]);

  return (
    <div
      className="content"
      onClick={() => {
        setTriggerState(triggerState + 1); //triggerState update
      }}
    >
      {content}
    </div>
  );
}

我已确认generateNewBody()、generateApplicationChars()和glitchController()逻辑符合预期,但当前代码中第二个useEffect似乎未触发,样式无法生效。我尝试过将函数移到组件外、合并useEffect、调整triggerState,但问题仍未解决。请问我是否对useEffect、状态变更或渲染概念存在误解?该如何修复以实现预期的Glitch动画效果?


解决方案

核心问题分析

  1. 静态JSX无法响应状态变化
    你在挂载时生成的content是固定的JSX节点,其中<span>的className会被锁定为创建时的glitching值(初始0),后续glitching状态更新时,这些静态节点不会重新计算样式类,自然看不到动画变化。

  2. 闭包导致状态更新异常
    glitchController中所有setTimeout使用的triggerState都是函数创建时的闭包值,多次setTriggerState(triggerState + 1)实际都是基于同一个初始值更新,状态变化不符合预期,进而导致监听triggerState的useEffect触发逻辑混乱。

  3. 无限循环被React抑制
    监听triggerState的useEffect会调用glitchController,而后者又会不断更新triggerState,形成无限循环。React会自动抑制这种异常触发,表现为你看到的“未触发”现象。

修复步骤

1. 分离数据与渲染逻辑

不再将JSX存入content状态,改为存储需要高亮的单词索引。每次渲染时,基于当前glitching状态动态生成带正确样式的<span>。

2. 使用函数式状态更新

调用setState时采用函数式更新(prev => prev + 1),确保每次状态更新都基于最新的前值,避免闭包陷阱。

3. 移除冗余的triggerState

动画控制仅依赖glitching状态即可,点击事件直接调用glitchController启动动画,无需通过triggerState间接触发。

完整修复代码

//GlitchingComponent.js
import * as React from "react";

export function GlitchingComponent({ children }) {
  const [glitching, setGlitching] = React.useState(0);
  // 存储需要高亮的单词索引,而非完整JSX
  const [highlightedIndices, setHighlightedIndices] = React.useState([]);

  const generateApplicationChars = (characters = 2) => {
    let output = [];
    for (let i = 0; i < characters; i++) {
      output.push(Math.floor(Math.random() * (36 - 10) + 10).toString(36));
    }
    return output;
  };

  // 挂载时仅计算一次需要高亮的单词索引
  React.useEffect(() => {
    const mainContentArray = children.split(" ");
    const regExps = generateApplicationChars(3).map(e => new RegExp(e, "i"));
    const indices = mainContentArray.reduce((acc, word, index) => {
      if (regExps.some(rexp => rexp.test(word))) {
        acc.push(index);
      }
      return acc;
    }, []);
    setHighlightedIndices(indices);
  }, [children]);

  // 动画控制器,使用函数式更新修复闭包问题
  const glitchController = () => {
    const timers = [];
    
    const updateGlitch = (value) => {
      setGlitching(value);
    };

    // 按时间序列设置动画状态
    timers.push(setTimeout(() => updateGlitch(1), 0));
    timers.push(setTimeout(() => updateGlitch(0), 133));
    timers.push(setTimeout(() => updateGlitch(1), 220));
    timers.push(setTimeout(() => updateGlitch(2), 350));
    timers.push(setTimeout(() => updateGlitch(3), 600));
    timers.push(setTimeout(() => updateGlitch(0), 750));
    timers.push(setTimeout(() => updateGlitch(3), 800));
    timers.push(setTimeout(() => updateGlitch(0), 1000));

    // 组件卸载时清除定时器,防止内存泄漏
    return () => timers.forEach(t => clearTimeout(t));
  };

  // 点击触发动画
  const handleClick = () => {
    const cleanup = glitchController();
    return cleanup;
  };

  // 动态生成渲染内容,每次渲染都会基于最新glitching状态计算样式
  const renderContent = () => {
    const mainContentArray = children.split(" ");
    return mainContentArray.map((word, index) => {
      if (highlightedIndices.includes(index)) {
        return (
          <span
            key={index}
            className={`toGlitch${glitching === 0 ? "" : ` glitching-${glitching}`}`}
          >
            {word + " "}
          </span>
        );
      }
      return word + " ";
    });
  };

  return (
    <div className="content" onClick={handleClick}>
      {renderContent()}
    </div>
  );
}

关键改进点

  • 动态渲染响应状态:通过renderContent函数每次渲染时生成JSX,确保<span>的className始终跟随glitching状态变化。
  • 避免闭包陷阱:使用函数式状态更新,保证状态更新基于最新值。
  • 简化逻辑:移除冗余的triggerState,直接通过点击事件启动动画,消除无限循环风险。
  • 内存泄漏防护:添加定时器清理逻辑,组件卸载时清除未执行的定时器。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 18:30:00