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
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 tomemoize
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.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.I utilized TypeScript b/c it was easier for me – you can just strip the type declarations for plain JavaScript code.
So, two takeways:
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 newSound
object on rerendering.number
) and less error prone if you start to get fancy.Let’s analyze the code to understand the problem:
You first have a state called playing:
and then you pass that state into all the ReactPlayer components you’re making via this part of the code:
So basically, all ReactPlayers are controlled directly by the single state, which means that either they are all
playing={true}
or they are allplaying={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.
Now add a condition in the
playing={...}
of each ReactPlayer.Notice the change here:
playing={soundId === id}
this will dynamically set the value for each ReactPlayer being created. Only the one withsoundId === 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.Notice the change: replacing
playSound(sound);
(we don’t need the function anymore) withsetSoundId(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.