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

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>
    );

}

关键优化点

  1. 将index改为状态变量,确保每次更新都能触发组件重新渲染,且状态值始终为最新。
  2. 使用setTimeout结合useEffect监听index,每次只调度一次字符添加,避免setInterval的累积误差和重复执行问题。
  3. 为useCallback和useEffect补充完整的依赖数组,符合React规则,避免不必要的重渲染和过时闭包。
  4. 增加定时器清理函数,确保组件卸载或重新渲染时清除旧定时器,防止内存泄漏和重复执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 05:15:00