useState() causes unexpected behaviour with audio object

Viewed 537

I'm trying to create a basic audio player and keeping track of the player state using the state hook.

The following code creates a component with the following behaviour:

  1. First time the button is pressed audio plays
  2. Every time the button is pressed the text toggles correctly
  3. When state is playing, calling player.pause() does nothing
  4. When state is not playing, audio continues and calling player.play() causes a second layer of audio to start on top
    import React, {useState} from 'react'
    
    function InlinePlayer ({audio}) {
      const [playing, setPlaying] = useState(false)
    
      const player = new Audio(audio.asset.url)
    
      function togglePlay () {
        playing ? player.pause() : player.play()
        setPlaying(!playing)
      }
    
      return <>
        <button onClick={() => togglePlay()}>
          {playing ? 'Stop' : 'Play' }
        </button>
      </>
    }
    
    export default InlinePlayer

If I don't use the state hook at all I can stop and start the audio without issues.

One strange thing is that even if I call play() unconditionally and then call the state hook, subsequent calls to pause() also don't work anymore. It's as if calling setPlaying() destroys the connection to the player object. If I comment out the setPlaying line, it works.

  function play () {
    player.play()
    setPlaying(true)
  }

  function pause () {
    player.pause()
    setPlaying(false)
  }

I initially thought the problem was that the state was being set asynchronously so the conditional play was the culprit. What seems to be up here?

2 Answers

player.pause() and player.play() are like side effects. Use useEffect with proper cleanup function so you can toggle the play/pause:

import React, { useState, useEffect } from "react";

function InlinePlayer({ audio }) {
  const [playing, setPlaying] = useState(false);

  const player = new Audio(
    "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
  );

  useEffect(() => {
    playing ? player.play() : player.pause();

    // This is cleanup of the effect
    return () => player.pause();

  }, [playing]);
   // ^ Run the effect every time the `playing` is changed

  function togglePlay() {
    // Using the callback version of `setState` so you always
    // toggle based on the latest state
    setPlaying(s => !s);
  }

  return (
    <>
      <button onClick={() => togglePlay()}>{playing ? "Stop" : "Play"}</button>
    </>
  );
}

Instancing your "Audio" in state hook:

const [ sound ] = useState(new Audio('path_sound'));

return (
  <div>
    <button onClick={() => sound.paused = !sound.paused}> 
      {sound.paused ? "Play" : "Stop"}
    </button>
  </div>
);
Related