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>; }
问题原因
- 父组件中
View通过useCallback定义,依赖数组包含timer:每秒timer更新时,View会重新创建,导致<PaymentSuccessView>/<PaymentFailView>被卸载后重新挂载,空依赖useEffect随之重复执行,触发音效播放。 - 父组件的定时器
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依赖
- 用
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;
- 修改父组件
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-
相关产品推荐
相关产品推荐

