React(NextJS)逐词动画失效问题求助
文本逐词展示动画的时序问题排查与修复
问题描述
要实现网页文本逐词展示的动画效果,采用state变量结合useEffect追加词语:调用useEffect写入第一个词到state,触发useEffect再次执行写入下一个词,渲染时直接展示该state变量。原本设置了间隔控制速度,现已注释用于测试。控制台中可见索引正常递增、词语从数组读取正确,但state变量中丢失第二个词;尝试逐字母实现时也存在第二个索引内容丢失,甚至更多内容混乱的情况。无论设置何种间隔都无法解决,疑似时序问题。
原代码
let index = useRef(0); const words = text.split(" "); useEffect(() => { const tick = () => { console.log("Index: ", index.current); console.log("Length: ", words.length); console.log("Char: ", words[index.current]); console.log("placeholder: ", placeholder); setPlaceholder((prev) => prev + " " + words[index.current]); index.current++; }; if (index.current < words.length - 1) { /* let addChar = setInterval(tick, 70); return () => clearInterval(addChar); */ tick(); } }, [placeholder]); ---some other stuff---- return <div>{placeholder}</div>;
问题分析
- 竞态条件:依赖
placeholder触发useEffect,当setPlaceholder还未完成state更新时,useEffect可能已再次执行,此时index.current已递增,但新的placeholder未生效,导致内容追加逻辑混乱,出现丢词/重复追加的情况。 - 边界判断错误:
index.current < words.length - 1会导致最后一个词无法被添加,应该判断index.current < words.length。 - 无缓存的
words:每次渲染都会重新执行text.split(" "),若text动态变化会引发不必要的重渲染,也可能导致依赖项不稳定。
修复方案
方案1:测试版(无间隔,解决时序问题)
import { useState, useEffect, useRef, useMemo } from 'react'; function TypingText({ text }) { const [placeholder, setPlaceholder] = useState(''); const index = useRef(0); // 缓存分割后的词语数组,避免重复计算 const words = useMemo(() => text.split(' '), [text]); useEffect(() => { const tick = () => { if (index.current >= words.length) return; console.log("Index: ", index.current); console.log("Word: ", words[index.current]); console.log("Current placeholder: ", placeholder); // 函数式更新确保拿到最新的prev值,同时trim()去除开头多余空格 setPlaceholder(prev => `${prev} ${words[index.current]}`.trim()); index.current++; }; tick(); }, [placeholder, words]); return <div>{placeholder}</div>; }
方案2:正式版(带间隔,彻底避免竞态)
不再依赖placeholder触发useEffect,改用定时器主动控制逐词追加,从根源解决时序问题:
import { useState, useEffect, useRef, useMemo } from 'react'; function TypingText({ text }) { const [placeholder, setPlaceholder] = useState(''); const index = useRef(0); const intervalRef = useRef(null); const words = useMemo(() => text.split(' '), [text]); useEffect(() => { // 文本变化时重置状态 setPlaceholder(''); index.current = 0; // 启动定时器控制逐词速度 intervalRef.current = setInterval(() => { if (index.current >= words.length) { clearInterval(intervalRef.current); return; } setPlaceholder(prev => `${prev} ${words[index.current]}`.trim()); index.current++; }, 70); // 组件卸载时清理定时器,避免内存泄漏 return () => clearInterval(intervalRef.current); }, [words]); return <div>{placeholder}</div>; }
关键修复点
- 用
useMemo缓存words数组,避免每次渲染重复分割字符串,提升性能并保证依赖稳定。 - 正式版改用定时器控制逻辑,不再依赖
placeholder触发useEffect,彻底消除竞态条件。 - 修复边界判断,确保所有词语都能被正确追加。
- 使用
trim()去除开头的多余空格,优化展示效果。 - 添加定时器清理函数,避免组件卸载后定时器继续运行导致内存泄漏。
内容的提问来源于stack exchange,提问作者Stefan T.
相关产品推荐
相关产品推荐

