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属性变化触发组件身份识别逻辑。
具体修复步骤
确保列表
key稳定唯一
目前使用i.Name作为key可行,但需保证歌曲名称无重复;若存在重复,改用后端返回的唯一ID(如i.id)作为key,避免React误判组件身份。将
width状态提升到父组件
进度条宽度与当前播放歌曲强关联,将状态移至父组件统一管理,即使组件重建也能获取正确值:- 父组件添加状态:
const [progressWidth, setProgressWidth] = useState(15); - 传递给
MaxOneSong:<MaxOneSong ... progressWidth={progressWidth} setProgressWidth={setProgressWidth} /> MaxOneSong中替换原有状态定义:const { progressWidth: width, setProgressWidth: setWidth } = props;
- 父组件添加状态:
修复
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]); // 添加正确依赖稳定
songs数组引用
若父组件中songs是动态生成的,用useMemo缓存避免每次渲染重新创建数组:const songs = useMemo(()=>{ // 生成songs数组的逻辑 }, [songData]); // 依赖项为生成数组的数据源
额外优化
用React Context统一管理播放状态(当前歌曲、播放状态、进度),避免多个组件分散管理状态,减少同步问题。
内容的提问来源于stack exchange,提问作者Max W.

