使用extraData与状态变量无法重新渲染FlatList问题
歌曲自动播放序列问题排查与修复
问题背景
基于Expo-AV API实现歌曲自动依次播放功能,包含App.js和ChildComponent两个组件。App.js通过FlatList渲染三首歌曲,用状态变量跟踪已播放歌曲,预期流程如下:
- 应用加载时
nextSongToPlay索引设为0 - FlatList通过Data数组渲染3个ChildComponent
- 每个ChildComponent接收renderItem的index和nextSongToPlay属性
- 第一个ChildComponent加载音频成功后,若index与nextSongToPlay相等,自动触发播放
- 歌曲播放完毕时,通过
onPlayBackStatusUpdate的didJustFinish属性触发App.js的NextSongToPlay方法 NextSongToPlay方法将nextSongToPlay状态变量递增1- 触发App.js及FlatList重新渲染(已将
nextSongToPlay传入FlatList的extraData属性) - 重新渲染后第二个ChildComponent的index与nextSongToPlay相等,自动播放歌曲
- 重复至最后一首歌曲
实际运行时,第一首歌播放完后nextSongToPlay状态变量已递增,但FlatList未触发后续歌曲自动播放。
现有代码
App.js
import { View, Text, FlatList } from "react-native"; import React, { useState, useRef, useEffect } from "react"; import ChildComponent from "./ChildComponent"; const Data = [ { key: "1", song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav", }, { key: "2", song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav", }, { key: "3", song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav", }, ]; export default function App() { const [nextSongToPlay, setNextSongToPlay] = useState(0); const shouldPlayOnItsOwn = useRef(false); useEffect(() => { return () => (shouldPlayOnItsOwn.current = false); }); const NextSongToPlay = () => { setNextSongToPlay(nextSongToPlay + 1); }; const setShouldPlayOnItsOwn = () => { shouldPlayOnItsOwn.current = true; }; const renderItem = ({ item, index }) => { return ( <View style={{ marginTop: 10 }}> <ChildComponent path={item.path} NextSongToPlay={() => NextSongToPlay()} nextSongToPlay={nextSongToPlay} index={index} songURL={item.song} setShouldPlayOnItsOwn={setShouldPlayOnItsOwn} shouldPlayOnItsOwn={shouldPlayOnItsOwn.current} /> </View> ); }; return ( <View style={{ justifyContent: "center", alignItems: "center", marginTop: 200 }} > <FlatList data={Data} renderItem={renderItem} extraData={nextSongToPlay} /> <Text style={{ marginTop: 30 }}> {" "} Number of Songs Played: {nextSongToPlay}{" "} </Text> </View> ); }
ChildComponent.js
import { View, Button } from "react-native"; import React, { useRef, useEffect } from "react"; import { Audio } from "expo-av"; export default function ChildComponent(props) { const sound = useRef(new Audio.Sound()); const PlayBackStatus = useRef(); const ref = useRef(); const alreadyPlayed = useRef(false); useEffect(() => { LoadAudio(); return () => sound.current.unloadAsync(); }, []); const LoadAudio = async () => { PlayBackStatus.current = sound.current .loadAsync({ uri: props.songURL }) .then((res) => { console.log(`load result : ${res}`); if (props.index === props.nextSongToPlay && props.shouldPlayOnItsOwn) { ref.current.props.onPress(); } }) .catch((err) => console.log(err)); }; const PlayAuido = async () => { alreadyPlayed ? sound.current.replayAsync() : (PlayBackStatus.current = sound.current .playAsync() .then(() => console.log(`result of playing: ${PlayBackStatus.current}`) ) .catch((err) => console.log(`PlayAsync Failed ${err}`))); }; sound.current.setOnPlaybackStatusUpdate((playBackObjectStatus) => { console.log( `Audio Finished Playing: ${playBackObjectStatus.didJustFinish}` ); if (playBackObjectStatus.didJustFinish) { console.log( `Inside the If Condition, Did the Audio Finished Playing?: ${playBackObjectStatus.didJustFinish}` ); alreadyPlayed.current = true; props.NextSongToPlay(); } }); const onPressHandler = () => { PlayAuido(); props.setShouldPlayOnItsOwn(); }; return ( <View> <Button title="Play Sound" onPress={onPressHandler} ref={ref} /> </View> ); }
问题原因分析
- ChildComponent未监听关键属性变化:ChildComponent的
useEffect依赖项为空数组,仅在组件挂载时执行一次LoadAudio。当nextSongToPlay更新后,组件重新渲染但不会再次检查播放条件。 shouldPlayOnItsOwn传递逻辑冗余且无效:将ref的current值作为props传递,该值无法触发组件重新渲染,导致属性更新后ChildComponent无法感知。- 状态更新存在闭包陷阱:
NextSongToPlay中直接使用nextSongToPlay的旧值,可能导致状态更新不及时。
修复方案
1. 修正App.js的状态更新方式
使用函数式更新避免闭包问题:
const NextSongToPlay = () => { setNextSongToPlay(prev => prev + 1); };
同时移除冗余的shouldPlayOnItsOwn相关代码,简化组件传参:
const renderItem = ({ item, index }) => { return ( <View style={{ marginTop: 10 }}> <ChildComponent NextSongToPlay={NextSongToPlay} nextSongToPlay={nextSongToPlay} index={index} songURL={item.song} /> </View> ); };
2. 重构ChildComponent的逻辑
分离音频加载和播放触发逻辑,监听关键属性变化:
import { View, Button } from "react-native"; import React, { useRef, useEffect, useState } from "react"; import { Audio } from "expo-av"; export default function ChildComponent(props) { const sound = useRef(new Audio.Sound()); const ref = useRef(); const alreadyPlayed = useRef(false); const [audioLoaded, setAudioLoaded] = useState(false); // 仅挂载时加载音频 useEffect(() => { const loadAudio = async () => { try { await sound.current.loadAsync({ uri: props.songURL }); setAudioLoaded(true); } catch (err) { console.log("加载音频失败:", err); } }; loadAudio(); return () => sound.current.unloadAsync(); }, []); // 监听nextSongToPlay和音频加载状态,触发自动播放 useEffect(() => { if (audioLoaded && props.index === props.nextSongToPlay) { ref.current.props.onPress(); } }, [audioLoaded, props.nextSongToPlay]); const PlayAudio = async () => { try { if (alreadyPlayed.current) { await sound.current.replayAsync(); } else { await sound.current.playAsync(); alreadyPlayed.current = true; } } catch (err) { console.log("播放音频失败:", err); } }; // 设置播放状态监听 useEffect(() => { const subscription = sound.current.setOnPlaybackStatusUpdate((status) => { if (status.didJustFinish) { props.NextSongToPlay(); } }); return () => subscription.remove(); }, []); const onPressHandler = () => { PlayAudio(); }; return ( <View> <Button title="Play Sound" onPress={onPressHandler} ref={ref} /> </View> ); }
修复后流程说明
- 所有ChildComponent挂载时加载对应音频,加载完成后标记
audioLoaded为true。 - 初始
nextSongToPlay为0,第一个ChildComponent检测到audioLoaded为true且index匹配,自动触发播放。 - 歌曲播放完毕,触发
NextSongToPlay使nextSongToPlay递增。 - FlatList因
extraData更新重新渲染,所有ChildComponent的nextSongToPlay属性同步更新。 - 第二个ChildComponent检测到index与
nextSongToPlay匹配且音频已加载,自动触发播放。 - 重复流程直到所有歌曲播放完毕。
内容的提问来源于stack exchange,提问作者Muhammad Elahi
相关产品推荐
相关产品推荐

