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

React列表组件状态随父组件state更新重置的解决方法

问题描述

父组件中有状态变量playing:

const [playing, setPlaying] = useState(songData[0] !== "none");

该状态传递给列表组件Songs:

function Songs(){
    return(
        songs.map(i=>{
            return(
                <MaxOneSong key={i.Name} Colors={Colors} audio={audio} name={i.Name} current={i.Name == playingSong} description={i.Description} playing={playing} setPlaying={setPlaying} playSong={()=>{
                    playSong(i.Name, `./audio/${i.File}.mp3`);
                }}/>
            );
        })
    );
}

核心问题:每当playing状态更新时,MaxOneSong组件内的状态变量(如width)会被重置为useState定义的初始值,但仅列表生成的组件存在该问题,其他子组件正常。

排查确认:给普通子组件添加状态后,验证问题仅出现在列表生成的组件中。

补充MaxOneSong组件代码(特意将width初始值设为15,验证状态重置由组件卸载导致):

export default function MaxOneSong({Colors, audio, current, name, description, playing, setPlaying, playSong}) {
    const [width, setWidth] = useState(15);

    const mouseOver = useRef(false);

    const timeBarRef = useRef();
    const timeBarContainerRef = useRef();

    useEffect(()=>{
        //If mouse moves, move timebar
        //TimebarContainer = 50vw, width: %

        timeBarContainerRef.current.addEventListener("mousemove", (e)=>{
            if(audio && current){
                const ww = window.innerWidth;
                if(e.clientX >= 0.23*ww && e.clientX <= 0.77*ww){
                    setWidth((e.clientX/ww-0.23)/0.0054);
                }
            }else{
                setWidth(0);
            }
        });
        timeBarContainerRef.current.addEventListener("mouseover", (e)=>{
            mouseOver.current = true;
        });
        return () => {
            if(timeBarContainerRef.current){
                timeBarContainerRef.current.removeEventListener("mousemove", (e)=>{
                    if(audio && current){
                        const ww = window.innerWidth;
                        if(e.clientX >= 0.23*ww && e.clientX <= 0.77*ww){
                            setWidth((e.clientX/ww-0.23)/0.0054);
                        }
                    }else{
                        setWidth(0);
                    }
                });
                timeBarContainerRef.current.removeEventListener("mouseover", (e)=>{
                    mouseOver.current = true;
                });
            }
        }
    }, []);


    useEffect(()=>{
        //Auto timebar moves

        if(audio && current){
            const intervalID = window.setInterval(() => {
                if(!mouseOver.current) {
                    if(audio.ended){
                        setWidth(100);
                        setPlaying(false);
                    }else{
                        setWidth(100 * audio.currentTime / audio.duration);
                    }
                }
            }, 1000);
            timeBarContainerRef.current.addEventListener("mouseleave", (e)=>{
                mouseOver.current = false;
                if(audio.ended){
                    setWidth(100);
                }else{
                    setWidth(100 * audio.currentTime / audio.duration);
                }
            });

            //Set time
            timeBarContainerRef.current.addEventListener("click", (e)=>{
                if(mouseOver.current) {
                    const ww = window.innerWidth;
                    audio.currentTime = audio.duration * ((e.clientX/ww-0.23)/0.54);
                }
            });

            return ()=>{
                clearInterval(intervalID);
                if(timeBarContainerRef.current){
                    timeBarContainerRef.current.removeEventListener("mouseleave", (e)=>{
                        mouseOver.current = false;
                        if(audio) {
                            if(audio.ended){
                                setWidth(100);
                            }else{
                                setWidth(100 * audio.currentTime / audio.duration);
                            }
                        }
                    });
                    timeBarContainerRef.current.removeEventListener("click", (e)=>{
                        if(audio && mouseOver.current) {
                            const ww = window.innerWidth;
                            audio.currentTime = audio.duration * ((e.clientX/ww-0.23)/0.54);
                        }
                    });
                }
            }
        }
    }, [audio]);


    const boxStyle = {
        outline: "0.3vmin solid",
        borderRadius: "2vw",
        width: "50vw",
        padding: "1vh 2vw 1vh 2vw",
        fontSize: "3vmin",
        position: "relative",
        zIndex: "1",
        backgroundColor: "rgba(0, 0, 0, .1)",
        display: "flex",
        flexDirection: "row",
        justifyContent: "space-between",
        cursor: "pointer",
        overflow: "hidden",
        height: current ? "4.5vh" : "4vh"
    }

    const timeBarStyle = {
        position: "absolute",
        left: "0",
        backgroundColor: "red",
        width: `${width}%`
    }

    const timeBarContainerStyle = {
        position: "absolute",
        left: "0",
        top: "88%",
        width: "100%",
        height: "12%"
    }
    
    return (
        <div style={boxStyle} onClick={()=>{
            if(current){
                setPlaying(!playing);
            }else{
                playSong();
            }
        }}>
            <div style={{fontWeight:"bold"}}>{name}</div>
            <div>{description}</div>
            <div style={timeBarContainerStyle} ref={timeBarContainerRef} className="timebarContainer">
                <div style={timeBarStyle} ref={timeBarRef} className="timebar">

                </div>
            </div>
        </div>
    )
}
解决方案

核心原因

playing状态更新触发父组件重渲染时,列表中的MaxOneSong组件被React判定为需要卸载重建而非更新,导致内部状态被重置。常见诱因包括:列表key不稳定、父组件中songs数组引用频繁变化、current属性变化触发组件身份识别逻辑。

具体修复步骤

  1. 确保列表key稳定唯一
    目前使用i.Name作为key可行,但需保证歌曲名称无重复;若存在重复,改用后端返回的唯一ID(如i.id)作为key,避免React误判组件身份。

  2. 将width状态提升到父组件
    进度条宽度与当前播放歌曲强关联,将状态移至父组件统一管理,即使组件重建也能获取正确值:

    • 父组件添加状态:const [progressWidth, setProgressWidth] = useState(15);
    • 传递给MaxOneSong:<MaxOneSong ... progressWidth={progressWidth} setProgressWidth={setProgressWidth} />
    • MaxOneSong中替换原有状态定义:const { progressWidth: width, setProgressWidth: setWidth } = props;
  3. 修复useEffect依赖项与事件绑定
    第一个useEffect依赖项为空数组,导致闭包捕获旧的audio和current值,同时事件监听移除逻辑无效。调整如下:

    // 提取事件处理函数
    const handleMouseMove = (e)=>{
        if(audio && current){
            const ww = window.innerWidth;
            if(e.clientX >= 0.23*ww && e.clientX <= 0.77*ww){
                setWidth((e.clientX/ww-0.23)/0.0054);
            }
        }else{
            setWidth(0);
        }
    };
    const handleMouseOver = (e)=>{
        mouseOver.current = true;
    };
    
    useEffect(()=>{
        timeBarContainerRef.current.addEventListener("mousemove", handleMouseMove);
        timeBarContainerRef.current.addEventListener("mouseover", handleMouseOver);
        return () => {
            timeBarContainerRef.current.removeEventListener("mousemove", handleMouseMove);
            timeBarContainerRef.current.removeEventListener("mouseover", handleMouseOver);
        }
    }, [audio, current]); // 添加正确依赖
    
  4. 稳定songs数组引用
    若父组件中songs是动态生成的,用useMemo缓存避免每次渲染重新创建数组:

    const songs = useMemo(()=>{
        // 生成songs数组的逻辑
    }, [songData]); // 依赖项为生成数组的数据源
    

额外优化

用React Context统一管理播放状态(当前歌曲、播放状态、进度),避免多个组件分散管理状态,减少同步问题。

内容的提问来源于stack exchange,提问作者Max W.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 08:03:10