React严格模式下,如何替代空数组useEffect实现仅渲染执行一次?
React Strict Mode下打字效果组件的修复方案
问题核心
你的组件在开发环境Strict Mode下异常,根源是Strict Mode会触发useEffect两次,而你的代码里wordIndex、letterIndex是组件顶层普通变量,两次挂载会生成两组独立的索引变量,同时启动两个异步打字/删除流程,互相干扰状态,导致效果混乱。
解决方案
要解决这个问题,需要从状态管理和流程控制两方面入手:
- 将索引变量转为React状态,确保全局唯一可追踪
- 给异步流程添加终止机制,避免重复启动的流程冲突
修复后的完整代码
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清理函数会终止旧流程,防止两个流程同时修改状态 - 重复启动拦截:通过
isRunningref标记活跃状态,确保同一时间只有一个打字流程在运行 - 依赖补全:useEffect依赖数组加入所有用到的props,确保props变化时能重新启动正确的流程
内容的提问来源于stack exchange,提问作者Benevos
相关产品推荐
相关产品推荐

