what is making props behavior inconsistent when set as a variable and passed into another function

I am creating a simple audio player using React.

When pushing the "Play" button, audio will be played from a url and the button will display "Pause". Once the audio is finished the button will reset to "Play."

There are three buttons "one" "two" "three". Each one will load a new audio url to be played by the button. Here is the codesandbox.

I'm new to useEffect and useState and I think there is an issue with how I'm updating this with props.

In my audio player component, if I directly set the url (without updating it with props), the audio will play as expected when the "play" button is pushed. However, when I set the url with props it won't play (even though the console.log() displays the correct url).

here is my Audio3.js component (responsible for playing audio:

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


const useAudio = (url) => {
  console.log("in useAudio", url)
  const [audio] = useState(new Audio(url));
  const [playing, setPlaying] = useState(false);


  const toggle = () => setPlaying(!playing);

  useEffect(() => {
    playing ? audio.play() : audio.pause();
  }, [playing]);

  useEffect(() => {
    audio.addEventListener("ended", () => setPlaying(false));
    return () => {
      audio.removeEventListener("ended", () => setPlaying(false));
    };
  }, []);

  return [playing, toggle];
};

const Player = (props) => {

console.log("the current page is", props.currPage)

  // const url = "https://github.com/cre8ture/audioFilesForBL/blob/main/2.mp3?raw=true"

  const url =  "https://github.com/cre8ture/audioFilesForBL/blob/main/" +
  props.currPage +
  ".mp3?raw=true"
  
  console.log("the audio 3 is ", url);
  const [playing, toggle] = useAudio(url);


  return (
    <div>
      <button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
    </div>
  );
};

export default Player;

Here is the threeButtons.js file:

import React from "react";

function Tabs(props) {
  return (
    <>
      <button onClick={() => props.handleChangeProps(1)}>ONE</button>
      <br />
      <button onClick={() => props.handleChangeProps(2)}>TWO</button>
      <br />
      <button onClick={() => props.handleChangeProps(3)}>THRE</button>
    </>
  );
}

export default Tabs;

Here is the header component that houses the audio Play button:

import React from "react";
import Audio from "./Audio3";

function Header(props) {
  console.log("header", props.currPage);

  return (
    <Audio currPage={props.currPage} />
  );
}

export default Header;

And lastly, here is my App.js

import React, { useState } from "react";
import Header from "./components/header";
import ConceptTabs from "./components/threeButtons";

function App() {
  const [pageID, setPageID] = useState(0);

  // goes to ConceptTabs
  let handleChange = (id) => {
    console.log("clicked", id);
    handleChange2(id);
    setPageID(id);
    return id;
  };

  // Goes to Audio2
  let handleChange2 = (id) => {
    console.log("clicked2", id);
    return id;
  };

  console.log("pageID", pageID);

  return (
    <>
      <Header currPage={pageID} />
      <br />
      <ConceptTabs handleChangeProps={handleChange} />
    </>
  );
}

export default App;

Thanks so much


Solution 1:

const [audio] = useState(new Audio(url));

is triggered only once when component mounts. If you want to change it when url change, you'll need to have a useEffect

const audio = useRef(new Audio(url));
useEffect(() => {
    audio.current = new Audio(url);
    // add listeners
    return () => {
        if (audio.current) {
            // Destroy audio & listeners
        }
    }
}, [url])

You'll also notice that I've used a useRef instead of useState for your audio, which is better when you need to store something which do not need a "re-render" when changing value