skip to Main Content

I just started with the learning of React.js and I’m trying to figure things out. I’m creating a simple meditation app where each button has a sound linked to it. I created 4 buttons (for each sound available) and when clicking on it, the sound plays. But obviously when clicking on other buttons the previous sound keeps playing in background. I have installed react-player to have more control over it and I tried to approach to a solution with useState.

Here’s the code:

import PropTypes from "prop-types";
import { useState } from "react";
import ReactPlayer from "react-player";

function Buttons({ soundsColor }) {
  const [alternativeText, setAlternativeText] = useState(null);
  const [playing, setPlaying] = useState(true);

  function playSound(soundUrl) {
    if (!playing) {
      soundUrl.play();
    } else {
      setPlaying(!playing);
    }

    console.log("Playing sound:", soundUrl);
  }

  function handleText(id) {
    setAlternativeText(id);
  }

  return (
    <>
      {/* Destructuring per rilevare le properties dall'oggetto */}
      {soundsColor.map(({ name, color, description, sound, id }, index) => (
        <button
          id={`btn-${index}`}
          className="p-20 relative rounded-[10px] active:scale-[0.98] transition-all 150ms ease-in "
          style={{ backgroundColor: color }}
          key={id}
          onClick={() => {
            playSound(sound);
            handleText(id);
          }}
        >
          <ReactPlayer
            url={sound}
            playing={playing}
            width="100%"
            height="100%"
          />
          <div className="absolute top-20 w-3/4 left-3 text-left">
            <p className="text-[#000] font-bold font-sourceSans">{name}</p>
            <small className="font-sourceSans text-secondSubtext font-light ">
              {alternativeText === id ? "Playing..." : description}
            </small>
          </div>
        </button>
      ))}
    </>
  );
}

Buttons.propTypes = {
  soundsColor: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number,
      name: PropTypes.string,
      description: PropTypes.string,
      color: PropTypes.string,
      sound: PropTypes.object,
    })
  ),
};

export function SoundScapes() {
  const soundsColor = [
    {
      id: 0,
      name: "Suikinkutsu",
      color: "#daff6f",
      description: "Water Drip Resonance",
      sound: new Audio("assets/Suikinkutsu.mp3"),
    },
    {
      id: 1,
      name: "Cicadas",
      color: "#A8AEEF",
      description: "Insect Chorus",
      sound: new Audio("./assets/Cicadas.mp3"),
    },
    {
      id: 2,
      name: "Temple Bells",
      color: "#A8AEEF",
      description: "Waterfall Roar",
      sound: new Audio("./assets/TempleBellSound.mp3"),
    },
    {
      id: 3,
      name: "Shomyo Falls",
      color: "#daff6f",
      description: "Bell Resonance",
      sound: new Audio("./assets/Waterfall.mp3"),
    },
  ];

  return (
    <div className="grid grid-cols-2 grid-rows-2 gap-3 p-6 mt-8">
      <Buttons soundsColor={soundsColor} />
    </div>
  );
}

I was thinking to create a :

const [playing,setPlaying] = useState(true) but I think I have to "attach" it to the playSound() function, which makes the sound play when clicking the button, and add a code that change the state when another button is pressed, but it’s where I’m stuck.

I would appreciate your help in understanding this, and if my approach is wrong. Thank you so much

3

Answers


  1. First of all, I’ld really recommend you to go with TypeScript instead of tinkering the PropTypes. The learning curve is not that steep, and it will save you lots of headache in the future.

    Second, I’ld recommend you not to modify existing prototypes when it can be avoided. There are almost (?) always other options, and stuff just gets messy otherwise.

    Okay, to your example then. Your hunch to utilize useState is correct, but you need to memoize the sounds so they won’t be recreated anew all the time. Here’s a sketch on how you can create the sounds, play them, and stop them. I left out the button logic b/c your questions looks like you know how to handle those.

    export const Buttons: React.FC = () => {
      // There are many ways to get a structure from id to sound. You could even use a plain object of type { [key:number]: ReturnType<typeof getSoundScapes>[number]>}.
      // You could even go with your array and search for the proper sound by id every time with map.find().
      // The important thing is that you create this list with useMemo and an empty dependency array, so on subsequent renderings the sounds won't be created anew.
      const soundScapes = useMemo(() => getSoundScapes().reduce(
        (prev, curr) => prev.set(curr.id, curr), new Map<number, ReturnType<typeof getSoundScapes>[number]>,
      ), []);
      // Here we store whether (and which) sound is playing currently
      const [playing, setPlaying] = React.useState<number|null>(null);
      // onClick handler is for your button to use. It will stop the currently playing sound (if any) and start the new one.
      // Additionally, the new sound will be stored as the one currently being played.
      const onClick = (idToPlay:number)=>{
        if (playing != null){
          const currentSound = soundScapes.get(playing);
          if (currentSound != null){
            // this only works b/c the songs are memoized - otherwise you would try to pause a new Sound instance which isn't even running
            currentSound.sound.pause();
            currentSound.sound.currentTime=0;
          }
        }
        const toPlay =soundScapes.get(idToPlay);
        if (toPlay != null){
          toPlay.sound.play();
          setPlaying(idToPlay);
        }
      }
    
      return Array.of(soundScapes.values()).map( ... create the buttons similar to your example)
    };
    

    I’ve modified the method which returns the sounds slightly. I doesn’t return some components anymore, but an Array. Of course, you could even return an object here, whose key is the id and whose value is the sound data. It’s all just a sketch, really. The core idea is that we return the sounds directy, not wrapped into an intermediate component, b/c we need to access them in the main component.

    const getSoundScapes = () =>  [
        {
          id: 0,
          name: 'Suikinkutsu',
          color: '#daff6f',
          description: 'Water Drip Resonance',
          sound: new Audio('assets/Suikinkutsu.mp3'),
        },
        {
          id: 1,
          name: 'Cicadas',
          color: '#A8AEEF',
          description: 'Insect Chorus',
          sound: new Audio('./assets/Cicadas.mp3'),
        },
        {
          id: 2,
          name: 'Temple Bells',
          color: '#A8AEEF',
          description: 'Waterfall Roar',
          sound: new Audio('./assets/TempleBellSound.mp3'),
        },
        {
          id: 3,
          name: 'Shomyo Falls',
          color: '#daff6f',
          description: 'Bell Resonance',
          sound: new Audio('./assets/Waterfall.mp3'),
        },
      ];
    

    I utilized TypeScript b/c it was easier for me – you can just strip the type declarations for plain JavaScript code.

    So, two takeways:

    1. memoize objects which shouldn’t be re-created on every rendering cycle. Like the sounds. You couln’t even stop the currently running one if you map points to a new Sound object on rerendering.
    2. Just use the id to refer to the playing sound – it’s easier to debug (b/c you can see which song is set currently just by checking a number) and less error prone if you start to get fancy.
    Login or Signup to reply.
  2. import PropTypes from "prop-types";
    import { useState, useRef } from "react";
    import ReactPlayer from "react-player";
    
    function ButtonsComponent({ soundsColor }) {
      const [alternativeText, setAlternativeText] = useState(null);
      const [playing, setPlaying] = useState(false);
      const [currentSound, setCurrentSound] = useState(null);
      const playerRef = useRef(null);
    
      function playSound(soundUrl) {
        if (currentSound && currentSound !== soundUrl) {
          playerRef.current.seekTo(0);
          setPlaying(false);
        }
    
        setCurrentSound(soundUrl);
        setPlaying(true);
    
        console.log("Playing sound:", soundUrl);
      }
    
      function handleText(id) {
        setAlternativeText(id);
      }
    
      return (
        <>
          {soundsColor.map(({ name, color, description, sound, id }, index) => (
            <button
              id={`btn-${index}`}
              className="p-20 relative rounded-[10px] active:scale-[0.98] transition-all 150ms ease-in"
              style={{ backgroundColor: color }}
              key={id}
              onClick={() => {
                playSound(sound);
                handleText(id);
              }}
            >
              <ReactPlayer
                ref={playerRef}
                url={currentSound}
                playing={playing && currentSound === sound}
                width="100%"
                height="100%"
              />
              <div className="absolute top-20 w-3/4 left-3 text-left">
                <p className="text-[#000] font-bold font-sourceSans">{name}</p>
                <small className="font-sourceSans text-secondSubtext font-light">
                  {alternativeText === id ? "Playing..." : description}
                </small>
              </div>
            </button>
          ))}
        </>
      );
    }
    
    ButtonsComponent.propTypes = {
      soundsColor: PropTypes.arrayOf(
        PropTypes.shape({
          id: PropTypes.number,
          name: PropTypes.string,
          description: PropTypes.string,
          color: PropTypes.string,
          sound: PropTypes.string, // Change to string type
        })
      ),
    };
    
    export function SoundScapes() {
      const soundsColor = [
        {
          id: 0,
          name: "Suikinkutsu",
          color: "#daff6f",
          description: "Water Drip Resonance",
          sound: "assets/Suikinkutsu.mp3", // Change to string URL
        },
        {
          id: 1,
          name: "Cicadas",
          color: "#A8AEEF",
          description: "Insect Chorus",
          sound: "./assets/Cicadas.mp3", // Change to string URL
        },
        {
          id: 2,
          name: "Temple Bells",
          color: "#A8AEEF",
          description: "Waterfall Roar",
          sound: "./assets/TempleBellSound.mp3", // Change to string URL
        },
        {
          id: 3,
          name: "Shomyo Falls",
          color: "#daff6f",
          description: "Bell Resonance",
          sound: "./assets/Waterfall.mp3", // Change to string URL
        },
      ];
    
      return (
        <div className="grid grid-cols-2 grid-rows-2 gap-3 p-6 mt-8">
          <Buttons soundsColor={soundsColor} />
        </div>
      );
    }
    
    Login or Signup to reply.
  3. Let’s analyze the code to understand the problem:

    You first have a state called playing:

    const [playing, setPlaying] = useState(true);
    

    and then you pass that state into all the ReactPlayer components you’re making via this part of the code:

              <ReactPlayer
                url={sound}
                playing={playing}
                width="100%"
                height="100%"
              />
    

    So basically, all ReactPlayers are controlled directly by the single state, which means that either they are all playing={true} or they are all playing={false}. Definitely not what you intended to do.

    What you want is to pick only one ReactPlayer and have its playing set to true while all others set to false. How can you do this?


    First you need something to identify which ReactPlayer should be playing. So let’s make a new state that contains the id of the Sound.

    const [soundId, setSoundId] = useState(0); //initially id: 0 will be on.
    

    Now add a condition in the playing={...} of each ReactPlayer.

    return (
        <>
          {/* Destructuring per rilevare le properties dall'oggetto */}
          {soundsColor.map(({ name, color, description, sound, id }, index) => (
            <button
              id={`btn-${index}`}
              className="p-20 relative rounded-[10px] active:scale-[0.98] transition-all 150ms ease-in "
              style={{ backgroundColor: color }}
              key={id}
              onClick={() => {
                playSound(sound);
                handleText(id);
              }}
            >
              <ReactPlayer
                url={sound}
                playing={soundId === id}
                width="100%"
                height="100%"
              />
              <div className="absolute top-20 w-3/4 left-3 text-left">
                <p className="text-[#000] font-bold font-sourceSans">{name}</p>
                <small className="font-sourceSans text-secondSubtext font-light ">
                  {alternativeText === id ? "Playing..." : description}
                </small>
              </div>
            </button>
          ))}
        </>
      );
    }
    

    Notice the change here: playing={soundId === id} this will dynamically set the value for each ReactPlayer being created. Only the one with soundId === id will evaluate to true and the rest to false.

    Now let’s add the logic for changing the soundId on clicking on a button.

            <button
              id={`btn-${index}`}
              className="p-20 relative rounded-[10px] active:scale-[0.98] transition-all 150ms ease-in "
              style={{ backgroundColor: color }}
              key={id}
              onClick={() => {
                setSoundId(id);
                handleText(id);
              }}
            >
    

    Notice the change: replacing playSound(sound); (we don’t need the function anymore) with setSoundId(id);.

    So now when you click a button, it will set the soundId to the new id, then the react component will refresh, turning only the ReactPlayer with a matching id to have its playing as true while all others as false.

    I hope this clarified the problem and the solution.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search