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

React setState更新Audio元素src正常但audioRef.current未更新问题求助

问题根因

  • new Audio(audioSrc)定义在组件顶层,仅组件首次渲染时生成的实例会被存入audioRef,后续组件重渲染(包括Context返回的tracks更新触发的重渲染)生成的新Audio实例不会同步到ref中
  • 你仅在trackIndex变化时更新audioRef,未监听tracks的变化,当Context更新曲目列表但当前trackIndex未变化时,不会触发音频实例的更新
  • trackIndex对应的useEffect执行顺序错误,先调用旧实例的play(),再替换成新的Audio实例,逻辑倒置

修复步骤

  1. 移除组件顶层的Audio实例创建逻辑,仅在需要更新音频资源时创建新实例
  2. 扩展useEffect的依赖项,加入tracks,确保曲目列表更新时也能同步音频资源
  3. 修正useEffect内的执行顺序,先替换新的Audio实例,再执行播放逻辑
  4. 新增旧音频实例的清理逻辑,避免内存泄漏和意外播放

修改后的核心代码片段

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.18 07:54:01