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

使用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>
  );
}

问题原因分析

  1. ChildComponent未监听关键属性变化:ChildComponent的useEffect依赖项为空数组,仅在组件挂载时执行一次LoadAudio。当nextSongToPlay更新后,组件重新渲染但不会再次检查播放条件。
  2. shouldPlayOnItsOwn传递逻辑冗余且无效:将ref的current值作为props传递,该值无法触发组件重新渲染,导致属性更新后ChildComponent无法感知。
  3. 状态更新存在闭包陷阱: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>
  );
}

修复后流程说明

  1. 所有ChildComponent挂载时加载对应音频,加载完成后标记audioLoaded为true。
  2. 初始nextSongToPlay为0,第一个ChildComponent检测到audioLoaded为true且index匹配,自动触发播放。
  3. 歌曲播放完毕,触发NextSongToPlay使nextSongToPlay递增。
  4. FlatList因extraData更新重新渲染,所有ChildComponent的nextSongToPlay属性同步更新。
  5. 第二个ChildComponent检测到index与nextSongToPlay匹配且音频已加载,自动触发播放。
  6. 重复流程直到所有歌曲播放完毕。

内容的提问来源于stack exchange,提问作者Muhammad Elahi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 19:05:22