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

父组件更新无关状态,子组件useEffect为何重复执行?

问题分析与解决方案

问题原因

  • Content组件内部定义的chatBlocks数组,会在组件每次重新渲染(比如speechText/nextSpeechTexts更新)时重新创建,导致传递给ChatItem的items prop引用每次都不相同。memo仅做浅比较,会判定props变化,触发ChatItem重新渲染。
  • ChatItem内部的useEffect依赖items,当items引用变化时,会执行清理函数清除原有定时器,重置visibleItems为空数组,然后重新启动定时器逐个加载消息,最终表现为“消息清空后重新逐个显示”。
  • Tts组件的useEffect未设置依赖项,每次组件渲染都会触发新的定时器,可能导致onEndSpeaking被重复调用。

修复步骤

  1. 稳定chatBlocks的引用:将chatBlocks移到Content组件外部,确保组件渲染时引用不变。
  2. 补全Tts组件的useEffect依赖:添加正确的依赖项,避免不必要的定时器触发。
  3. 优化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:23:19