1

React Newbie here,

import React, { Component } from "react";

class AudioList extends Component {
  constructor(props) {
    super(props);

    this.audios = [];
    this.buttonText = [];
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios.push(new Audio(this.props.songs[i].song_url));
      this.buttonText.push(String(i));
    }

    this.state = {
      songs: "",
      buttonText: this.buttonText
    };
  }

  componentWillMount() {
    const songs = [];
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios[i].addEventListener("play", () => {
        let stateArray = [...this.state.buttonText];
        let stateArrayElement = { ...stateArray[i] };
        stateArrayElement = "playing";
        stateArray[i] = stateArrayElement;
        console.log(stateArray);
        this.setState({ buttonText: stateArray });
        console.log(this.state.buttonText[i]);
      });
      songs.push(
        <div className="song-preview">
          <button
            className="preview"
            onClick={() => this.toggle(this.audios[i])}
          >
            {this.state.buttonText[i]}
          </button>
        </div>
      );
    }

    this.setState({
      songs: songs
    });
  }

  componentWillUnmount() {
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios[i].pause();
    }
  }

  getCurrentAudio() {
    return this.audios.find(audio => false === audio.paused);
  }

  toggle(nextAudio) {
    const currentAudio = this.getCurrentAudio();

    if (currentAudio && currentAudio !== nextAudio) {
      currentAudio.pause();
      nextAudio.play();
    }

    nextAudio.paused ? nextAudio.play() : nextAudio.pause();
  }

  render() {
    if (this.state.songs) {
      return <div className="song-list">{this.state.songs}</div>;
    } else {
      return <div className="song-list"></div>;
    }
  }
}

export default AudioList;

I am using this code from a previous solution that I found on Stackoverflow (https://stackoverflow.com/a/50595639). I was able to implement this solution to solve my own challenge of needing to have multiple audio sources with one audio player and multiple buttons. However, I am now faced with a new challenge - I want a specific button's text to change when an event is fired up.

I came up with this implementation where the button text is based on an array in the state called buttonText. The buttons are rendered correctly on startup, but when the event listener picks up the event and changes the state, the text in the button is not re-rendering or changing, even though it is based on an element in an array in the state that is changing.

Does anyone have any suggestions about why it may be failing to re-render?

EDIT: Changing an individual array element in the state is based on React: how to update state.item[1] in state using setState?

Raghul SK
  • 1,256
  • 5
  • 22
  • 30
Nate
  • 314
  • 1
  • 2
  • 11

2 Answers2

1

I have restructured your code a bit (but it's untested it's tested now):

const songs = [
  { 
    title: "small airplane long flyby - Mike_Koenig",
    song_url: "http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3" 
  },
  { 
    title: "Female Counts To Ten",
    song_url: "http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3" 
  },
];

class AudioList extends React.Component {
  audios = new Map();

  state = {
    audio: null,
    song: null
  };

  componentDidUpdate() {
    // stop playing if the song has been removed from props.songs
    if (this.state.song && !this.props.songs.some(song => song.song_url === this.state.song.song_url)) {
      this.toggle(this.state.audio, this.state.song);
    }
  }

  componentWillUnmount() {
    if (this.state.audio) {
      this.state.audio.pause();
    }
    this.audios.clear();
  }

  toggle(audio, song) {
    this.setState(state => {
      if (audio !== state.audio) {
        if (state.audio) {
          state.audio.pause();
        }
        audio.play();

        return { audio, song };
      }

      audio.pause();
      return { audio: null, song: null };
    });
  }

  getAudio(song) {
    let audio = this.audios.get(song.song_url);
    if (!audio) {
      this.audios.set(song.song_url, audio = new Audio(song.song_url));
    }
    return audio;
  }

  render() {
    return <div className="song-list">{
      this.props.songs.map((song, i) => {
        const audio = this.getAudio(song);
        const playing = audio === this.state.audio;

        return <div className="song-preview">
          <button
            className="preview"
            onClick={this.toggle.bind(this, audio, song)}
          >
            {playing ? "playing" : (song.title || i)}
          </button>
        </div>
      })
    }</div>;
  }
}

ReactDOM.render(<AudioList songs={songs} />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Edit: added a title to the song-objects and display them on the buttons

Thomas
  • 11,958
  • 1
  • 14
  • 23
  • Hi @Thomas, thank you for your answer. I tested your code. It runs and functions, however its functionality is incorrect. It will change the text on each button as opposed to the specific button that was pressed. – Nate Jul 07 '20 at 18:12
  • 1
    @Nate, made the code block executable. As far as I can tell, it works exactly as it is supposed to. What did you pass as `source`? In your question this is was a list of objects with a property `song_url`, whereas in your answer you seem to pass a list of urls(strings). That may be what's causing this bug. The code determines the `Audio` object for a button by said `song_url` and the `playing` state by that Audio-object. – Thomas Jul 07 '20 at 18:49
  • Wow @Thomas, this is amazing. Thank you. I ran it and it works perfectly fine as expected. And yes, you are most likely correct about the bug. My audio source was a list of objects with a property called song_url. So your implementation of the audio was perfectly done. I am still new to React so I will be spending some time understanding how your code functions and how to implement other features like changing the text after a song stops and what not. Thank you so much for taking the time to re-structure the code and essentially re-write it. Very appreciated. – Nate Jul 08 '20 at 03:06
0

I simply moved all of the code from componentWillMount() to render(). I also removed 'songs' as a state variable and set it to a variable that exists only in render as songs is simply just a set of divs.

import React, { Component } from "react";

const audio1 =
  "http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3";
const audio2 =
  "http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3";

class AudioList extends Component {
  constructor(props) {
    super(props);

    this.audios = [];
    this.buttonText = [];
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios.push(new Audio(this.props.songs[i]));
      this.buttonText.push(String(i));
    }

    this.state = {
      buttonText: this.buttonText
    };
  }

  componentWillUnmount() {
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios[i].pause();
    }
  }

  getCurrentAudio() {
    return this.audios.find(audio => false === audio.paused);
  }

  toggle(nextAudio) {
    const currentAudio = this.getCurrentAudio();

    if (currentAudio && currentAudio !== nextAudio) {
      currentAudio.pause();
      nextAudio.play();
    }

    nextAudio.paused ? nextAudio.play() : nextAudio.pause();
  }

  render() {
    const songs = [];
    for (let i = 0; i < this.props.songs.length; i++) {
      this.audios[i].addEventListener("play", () => {
        console.log("playing");
        let stateArray = [...this.state.buttonText];
        let stateArrayElement = { ...stateArray[i] };
        stateArrayElement = "playing";
        stateArray[i] = stateArrayElement;
        console.log(stateArray);
        this.setState({ buttonText: stateArray });
        console.log(this.state.buttonText);
      });
      songs.push(
        <div className="song-preview">
          <button
            className="preview"
            onClick={() => this.toggle(this.audios[i])}
          >
            {this.state.buttonText[i]}
          </button>
        </div>
      );
    }

    return (
        <div>{songs}</div>
    )
  }
}

export default () => <AudioList songs={[audio1, audio2]} />;

The code now runs as expected.

Nate
  • 314
  • 1
  • 2
  • 11
  • 1
    The `addEventListener` inside a loop in the `render` function performing a `setState` should pretty quickly become performance problem. – Thomas Jul 07 '20 at 06:47
  • @Thomas Yes, that is a very good point. Thank you for picking up on that. I will implement some potential fixes tomorrow and update my solution when I find one that works and is fast. – Nate Jul 07 '20 at 07:05
  • @Thomas Hi Thomas, yes you're completely right. There are major performance issues even if the list only contains 5 or so elements. I unfortunately can't think of a way to manipulate the array as arrays in state are immutable unless you re-copy it over completely, so I may have to go back to the drawing board for this one. – Nate Jul 07 '20 at 18:07