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

React严格模式下,如何替代空数组useEffect实现仅渲染执行一次?

React Strict Mode下打字效果组件的修复方案

问题核心

你的组件在开发环境Strict Mode下异常,根源是Strict Mode会触发useEffect两次,而你的代码里wordIndex、letterIndex是组件顶层普通变量,两次挂载会生成两组独立的索引变量,同时启动两个异步打字/删除流程,互相干扰状态,导致效果混乱。

解决方案

要解决这个问题,需要从状态管理和流程控制两方面入手:

  1. 将索引变量转为React状态,确保全局唯一可追踪
  2. 给异步流程添加终止机制,避免重复启动的流程冲突

修复后的完整代码

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

function TestComponent(
    { 
        words,
        typeDelay, 
        eraseDelay,
        nextWordDelay,
    } 
    :
    { 
        words: string[],
        typeDelay: number,
        eraseDelay: number,
        nextWordDelay: number,
    }
) 
{
    const [text, setText] = useState("");
    // 将索引存入React状态,确保状态唯一同步
    const [wordIndex, setWordIndex] = useState(0);
    const [letterIndex, setLetterIndex] = useState(0);
    // 标记当前是否有活跃的打字流程,防止重复启动
    const isRunning = useRef(false);
    // 用于终止异步流程的控制器
    const abortController = useRef(new AbortController());

    const sleep = (delay: number) => new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, delay);
        // 监听终止信号,触发时清除定时器并终止Promise
        abortController.current.signal.addEventListener('abort', () => {
            clearTimeout(timer);
            reject(new Error('流程已终止'));
        });
    });

    const type = async () => {  
        if (abortController.current.signal.aborted) return;
        
        const currentWord = words[wordIndex];
        if(letterIndex < currentWord.length)
        {
            const newLetter = currentWord.charAt(letterIndex);
            setText(prev => prev + newLetter);
            setLetterIndex(prev => prev + 1);
            
            await sleep(typeDelay);
            type();
            return;
        }

        await sleep(nextWordDelay);
        erase();
    }

    const erase = async () => {
        if (abortController.current.signal.aborted) return;
        
        const currentWord = words[wordIndex];
        if(letterIndex > 0)
        {
            const newText = currentWord.substring(0, letterIndex - 1);
            setText(newText);
            setLetterIndex(prev => prev - 1);
            
            await sleep(eraseDelay);
            erase();
            return;
        }

        // 循环切换单词索引
        setWordIndex(prev => (prev + 1) % words.length);
        await sleep(nextWordDelay);
        type();
    }

    useEffect(() => {
        // 仅当无活跃流程时启动
        if (!isRunning.current) {
            isRunning.current = true;
            type().catch(() => {});
        }

        // 组件卸载/重新渲染时,终止当前流程并重置状态
        return () => {
            abortController.current.abort();
            abortController.current = new AbortController();
            isRunning.current = false;
            setText("");
            setWordIndex(0);
            setLetterIndex(0);
        };
    }, [words, typeDelay, eraseDelay, nextWordDelay])

    return (
        <p>
            <span className='typed-text'>
                {text}
            </span>
        </p>
    )
}

export default TestComponent

关键改动说明

  • 状态迁移:把wordIndex、letterIndex从普通变量转为React状态,确保所有流程共享同一组索引,避免状态分裂
  • 流程终止:用AbortController监听终止信号,Strict Mode二次挂载时,第一次的useEffect清理函数会终止旧流程,防止两个流程同时修改状态
  • 重复启动拦截:通过isRunning ref标记活跃状态,确保同一时间只有一个打字流程在运行
  • 依赖补全:useEffect依赖数组加入所有用到的props,确保props变化时能重新启动正确的流程

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 13:12:51