父组件更新无关状态,子组件useEffect为何重复执行?
问题分析与解决方案
问题原因
Content组件内部定义的chatBlocks数组,会在组件每次重新渲染(比如speechText/nextSpeechTexts更新)时重新创建,导致传递给ChatItem的itemsprop引用每次都不相同。memo仅做浅比较,会判定props变化,触发ChatItem重新渲染。ChatItem内部的useEffect依赖items,当items引用变化时,会执行清理函数清除原有定时器,重置visibleItems为空数组,然后重新启动定时器逐个加载消息,最终表现为“消息清空后重新逐个显示”。Tts组件的useEffect未设置依赖项,每次组件渲染都会触发新的定时器,可能导致onEndSpeaking被重复调用。
修复步骤
- 稳定chatBlocks的引用:将
chatBlocks移到Content组件外部,确保组件渲染时引用不变。 - 补全Tts组件的useEffect依赖:添加正确的依赖项,避免不必要的定时器触发。
- 优化ChatItem的状态更新逻辑:使用函数式更新减少对外部变量的依赖,提升组件稳定性。
修改后的代码
import { memo, useCallback, useEffect, useState } from "react" const Tts = memo(({text, index, disabled, onStartSpeaking, onEndSpeaking}: {text: string, index: number, disabled?: boolean, onStartSpeaking?: () => void, onEndSpeaking?: () => void}) => { useEffect(() => { const timer = setTimeout(() => { onEndSpeaking?.() }, 5000) return () => clearTimeout(timer) }, [onEndSpeaking]) // 添加依赖,仅当回调变化时重新触发定时器 const styles = [ 'bg-red-200', 'bg-yellow-200', 'bg-green-200', 'bg-blue-200', ] return ( <div className={styles[index % styles.length]}>Reading: {index}-{text}</div> ) }) const ChatItem = memo(({items, slowPop}: {items: string[], slowPop?: boolean}) => { const [visibleItems, setVisibleItems] = useState<string[]>([]) useEffect(() => { if (slowPop) { setVisibleItems([]) const timeouts = items.map((item, index) => { const delay = 1500 * index return setTimeout(() => { // 用函数式更新避免直接依赖items setVisibleItems(prev => [...prev, item]) }, delay) }) return () => timeouts.forEach(timeout => clearTimeout(timeout)) } else { setVisibleItems(items) } }, [items, slowPop]) return ( <div className="chat-item"> <ul> {visibleItems.map((item, index) => ( <li key={index} className="chat-item-text animate-fadeIn"> {item} </li> ))} </ul> </div> ) }) // 将chatBlocks移到组件外部,保证引用始终稳定 const chatBlocks = [ [ 'previous chat history...', 'previous chat history...', ], [ 'I am doing well too.', 'What are you up to today?', 'Just working on some projects.', ], ] function Content() { type SpeechTextMeta = { index: number text: string } const [speechText, setSpeechText] = useState<SpeechTextMeta>({ index: 0, text: 'This is a test, and I will speak for a while.', }) const [nextSpeechTexts, setNextSpeechTexts] = useState<SpeechTextMeta[]>([ { index: 1, text: 'This is the followed text.' }, ]) const handleEndSpeaking = useCallback(() => { setNextSpeechTexts(prev => { if (prev.length > 0) { setSpeechText(prev[0]) return prev.slice(1) } return prev }) }, []) return ( <div> <Tts text={speechText.text} index={speechText.index} onEndSpeaking={handleEndSpeaking} disabled={false} /> <ul> {chatBlocks.map((block, index, a) => ( <li key={index} className="chat-item-title py-2"> <ChatItem items={block} slowPop={index === a.length - 1} /> </li> ))} </ul> </div> ) } export default function Page() { const [show, setShow] = useState(false) return ( <div> <button onClick={() => setShow(!show)} className="border border-gray-400 shadow-lg cursor-pointer active:bg-gray-500">Toggle</button> {show && <Content />} </div> ) }
额外优化说明:
handleEndSpeaking改用函数式更新状态,移除了对nextSpeechTexts的依赖,让回调引用始终稳定。ChatItem中用函数式更新visibleItems,避免直接依赖外部的items数组,进一步降低重新渲染的触发概率。
内容的提问来源于stack exchange,提问作者Programus
相关产品推荐
相关产品推荐

