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

useCallback内渲染组件的空依赖useEffect失效,音效重复播放问题

支付模态框音效重复播放问题修复

问题描述

实现支付页面模态框:点击“PAY”按钮后先显示加载状态,随后渲染支付成功/失败组件并播放对应音效。当前使用空依赖useEffect实现挂载时仅播放一次音效,但实际音效会随timer每秒更新而重复播放。

相关代码

父组件代码:

export default ProcessPaymentModal({ isSuccess, isError, isLoading }){
    const [timer, setTimer] = useState(0)
    
    useEffect(()=> { 
        const intervalId = setInterval(()=> setTimer((previousState)=> previousState + 1), 1000)
        return ()=> clearInterval(intervalId)
    })

    const View = useCallback(()=>{
      switch(true){
         case isSuccess:
           return <PaymentSuccessView timer={timer}/>
         case isError:
           return <PaymentFailView timer={timer}/>
         case isLoading:
           return <LoadingView />
      }
    }, [timer, isSuccess, isError, isLoading])

    return (
       <React.Fragment>
          some content
          <View />
       </React.Fragment>
    )
}

支付成功组件代码:

export default function PaymentSuccessView({ timer }) {
  useEffect(() => {
    const soundEffect = new Audio('../media/checktennn.mp3')
    soundEffect.play()
  }, []);
  return <button> OK ({timer}) </button>;
}

问题原因

  1. 父组件中View通过useCallback定义,依赖数组包含timer:每秒timer更新时,View会重新创建,导致<PaymentSuccessView>/<PaymentFailView>被卸载后重新挂载,空依赖useEffect随之重复执行,触发音效播放。
  2. 父组件的定时器useEffect无依赖数组,会在每次组件渲染时重新创建定时器,存在性能隐患。

修复方案

方案1:移除useCallback,直接条件渲染

去掉View的useCallback定义,直接在render中进行条件判断,避免组件重复卸载挂载:

export default function ProcessPaymentModal({ isSuccess, isError, isLoading }){
    const [timer, setTimer] = useState(0)
    
    // 给定时器useEffect加空依赖,只初始化一次
    useEffect(()=> { 
        const intervalId = setInterval(()=> setTimer((prev)=> prev + 1), 1000)
        return ()=> clearInterval(intervalId)
    }, [])

    return (
       <React.Fragment>
          some content
          {isSuccess && <PaymentSuccessView timer={timer}/>}
          {isError && <PaymentFailView timer={timer}/>}
          {isLoading && <LoadingView />}
       </React.Fragment>
    )
}

方案2:用React.memo包裹子组件,优化useCallback依赖

  1. 用React.memo包裹子组件,避免props更新时组件卸载重挂载:
import { memo, useEffect } from 'react';

const PaymentSuccessView = memo(function PaymentSuccessView({ timer }) {
  useEffect(() => {
    const soundEffect = new Audio('../media/checktennn.mp3')
    soundEffect.play()
  }, []);
  return <button> OK ({timer}) </button>;
});

export default PaymentSuccessView;
  1. 修改父组件View的useCallback依赖,移除timer:
const View = useCallback(()=>{
  switch(true){
     case isSuccess:
       return <PaymentSuccessView timer={timer}/>
     case isError:
       return <PaymentFailView timer={timer}/>
     case isLoading:
       return <LoadingView />
  }
}, [isSuccess, isError, isLoading]) // 移除timer依赖,避免View重复创建

额外优化:确保音效仅播放一次

通过useRef标记是否已播放,即使组件意外重挂载也能保证音效只触发一次:

import { memo, useEffect, useRef } from 'react';

// 缓存音效实例,避免重复创建
const successSound = new Audio('../media/checktennn.mp3');

const PaymentSuccessView = memo(function PaymentSuccessView({ timer }) {
  const hasPlayed = useRef(false);
  
  useEffect(() => {
    if (!hasPlayed.current) {
      successSound.play();
      hasPlayed.current = true;
    }
  }, []);

  return <button> OK ({timer}) </button>;
});

export default PaymentSuccessView;

内容的提问来源于stack exchange,提问作者Islam Y-

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 20:40:34