React中字符重复打印问题:为何每个字符被输出两次?
React逐字显示文本时字符重复两次的问题根源与修复方案
问题现象
在React中实现逐字显示文本效果时,出现每个字符被重复打印两次的问题,需要找到根本原因并给出合理修复,拒绝index % 2这类临时 workaround。
问题根源
核心原因是React严格模式(React.StrictMode)在开发环境下会重复执行组件的副作用(useEffect)和回调函数,再加上原始代码存在三个关键问题:
- 用闭包变量
index跟踪字符位置,严格模式下两次执行useEffect会创建两个独立的定时器,每个定时器都在修改outText状态,导致字符重复。 useCallback未指定依赖数组,每次组件渲染都会重新创建drawText函数,进一步加剧重复执行的问题。- 定时器未在组件卸载或重新渲染时正确清理,导致旧定时器继续运行。
原始代码
index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import Test from './test.js'; function Root(){ return( <React.StrictMode> <Test/> </React.StrictMode> ) } ReactDOM.createRoot(document.getElementById('root')).render(<Root/>);
test.js
import {useCallback,useEffect,useState} from 'react'; export default function Test(){ const [outText,setOutText] = useState(''); const drawText = useCallback(text=>{ let index=0; let textInterval; const callback = ()=>{ if (index>=text.length-1){ clearInterval(textInterval); return; } setOutText(t=>{ if (text[index]==='\n'){ return t+'<br>'; } else { return t+text[index]; } }); index++; }; textInterval=setInterval(callback,20); }); useEffect(()=>{ const text = ` a text is written here and should be printed step by step `.split(''); drawText(text); },[]) return ( <div id="test"> <div className="out-text" dangerouslySetInnerHTML={{__html:outText}} /> </div> ); }
修复方案
修复思路:
- 用React状态
index跟踪当前字符位置,替代闭包变量,避免闭包捕获过时值的问题。 - 使用
setTimeout代替setInterval,配合useEffect监听index变化,每次更新后调度下一次字符添加,同时在清理函数中清除定时器。 - 确保副作用可被正确清理,避免严格模式下重复执行导致的多个定时器冲突。
修复后的test.js
import {useCallback,useEffect,useState} from 'react'; export default function Test(){ const [outText,setOutText] = useState(''); const [index,setIndex] = useState(-1); const text = ` any text is written here and should be printed step by step `.split('') const mapChar = useCallback(c=>{ if (c==='\n'){ return '<br/>'; } else{ return c; } }, []); useEffect(()=>{ if(index<0|| index>=text.length){ return; } let timeout; const delay = index === 0 ? 500 : 20; timeout=setTimeout(()=>{ setIndex(i=>i+1); setOutText(t=>t+mapChar(text[index])); }, delay); return ()=> clearTimeout(timeout); },[index, text, mapChar]); // 触发逐字显示的入口(可根据实际场景替换,比如滚动触发) useEffect(()=>{ setIndex(0); },[]) return ( <div id="test"> <div className="out-text" dangerouslySetInnerHTML={{__html:outText}} /> </div> ); }
关键优化点
- 将
index改为状态变量,确保每次更新都能触发组件重新渲染,且状态值始终为最新。 - 使用
setTimeout结合useEffect监听index,每次只调度一次字符添加,避免setInterval的累积误差和重复执行问题。 - 为
useCallback和useEffect补充完整的依赖数组,符合React规则,避免不必要的重渲染和过时闭包。 - 增加定时器清理函数,确保组件卸载或重新渲染时清除旧定时器,防止内存泄漏和重复执行。
内容的提问来源于stack exchange,提问作者bitwiseio
相关产品推荐
相关产品推荐

