React setState更新Audio元素src正常但audioRef.current未更新问题求助
问题根因
new Audio(audioSrc)定义在组件顶层,仅组件首次渲染时生成的实例会被存入audioRef,后续组件重渲染(包括Context返回的tracks更新触发的重渲染)生成的新Audio实例不会同步到ref中- 你仅在
trackIndex变化时更新audioRef,未监听tracks的变化,当Context更新曲目列表但当前trackIndex未变化时,不会触发音频实例的更新 trackIndex对应的useEffect执行顺序错误,先调用旧实例的play(),再替换成新的Audio实例,逻辑倒置
修复步骤
- 移除组件顶层的Audio实例创建逻辑,仅在需要更新音频资源时创建新实例
- 扩展useEffect的依赖项,加入
tracks,确保曲目列表更新时也能同步音频资源 - 修正useEffect内的执行顺序,先替换新的Audio实例,再执行播放逻辑
- 新增旧音频实例的清理逻辑,避免内存泄漏和意外播放
修改后的核心代码片段
import React, { useState, useEffect, useRef, useContext } from 'react' import { TrackContext } from '../../music/TrackContext' const MusicPlayer = () => { const [tracks, setTracks] = useContext(TrackContext) // states const [trackIndex, setTrackIndex] = useState(0) const [trackProgress, setTrackProgress] = useState(0) const [isPlaying, setIsPlaying] = useState(false) const [volume, setVolume] = useState(1) const { title, artist, image, audioSrc } = tracks[trackIndex] || {} //refs // 移除顶层Audio创建逻辑,ref初始赋值为空 const audioRef = useRef(null) const intervalRef = useRef() const isReady = useRef(false) // 待音频元数据加载后再取duration,避免取值异常 const duration = audioRef.current?.duration || 0 const toPrevTrack = () => { if (trackIndex - 1 < 0) { setTrackIndex(tracks.length - 1) } else { setTrackIndex(trackIndex - 1) } } const toNextTrack = () => { if (trackIndex < tracks.length - 1) { setTrackIndex(trackIndex + 1) } else { setTrackIndex(0) } } const startTimer = () => { clearInterval(intervalRef.current) intervalRef.current = setInterval(() => { if (audioRef.current?.ended) { toNextTrack() } else if(audioRef.current) { setTrackProgress(audioRef.current.currentTime); } }, 1000) } useEffect(() => { if (isPlaying && audioRef.current) { audioRef.current.play() startTimer(); } else { clearInterval(intervalRef.current) audioRef.current?.pause() } }, [isPlaying]) useEffect(() => { return () => { audioRef.current?.pause() clearInterval(intervalRef.current) } }, []) // 新增tracks、audioSrc作为依赖,曲目列表更新时也触发同步 useEffect(() => { // 先清理旧实例 if (audioRef.current) { audioRef.current.pause() audioRef.current = null } if(!audioSrc) return // 再创建新实例 const newAudio = new Audio(audioSrc) audioRef.current = newAudio setTrackProgress(0) if (isReady.current) { newAudio.play() setIsPlaying(true) startTimer() } else { isReady.current = true } return () => { newAudio.pause() } }, [trackIndex, tracks, audioSrc]) const onScrub = (value) => { if(!audioRef.current) return clearInterval(intervalRef.current) audioRef.current.currentTime = value setTrackProgress(audioRef.current.currentTime) } const onScrubEnd = () => { if (!isPlaying) { setIsPlaying(true); } startTimer(); } const onScrubVolume = (value) => { if(!audioRef.current) return audioRef.current.volume = value setVolume(value) } function formatMinutes(sec) { return new Date(sec * 1000).toUTCString().split(" ")[4].substr(3, 8) } const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%'; const trackStyling = `-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}, #777))`; return ( <div className="player"> <div className="left-block"> <div className="art"> <img src={image} alt="" /> </div> <div className="song-details"> <div className="song-name">{title}</div> <div className="artist-name"> <a href="sa">{artist}</a> </div> </div> </div> <div className="center-block"> <div className="song-progress"> <div>{formatMinutes(audioRef.current?.currentTime || 0)}</div> <input value={trackProgress} step="1" min="0" max={duration || 0} onChange={(e) => onScrub(e.target.value)} onMouseUp={onScrubEnd} onKeyUp={onScrubEnd} style={{ background: trackStyling }} type="range" /> <div>{duration ? formatMinutes(duration) : "00:00"}</div> </div> </div> </div> ) } export default MusicPlayer
额外优化说明
- 新增了可选链操作符判断
audioRef.current是否存在,避免实例未初始化时报错 - 调整了进度条的min属性为0,避免初始进度显示异常
- 音频资源切换时自动重置进度为0,符合常规播放器的交互逻辑
内容的提问来源于stack exchange,提问作者Amisha Mundra
相关产品推荐
相关产品推荐

