I have a component that instantiates a few classes from the Tone.js library (e.g audio players and filters) and defines a few functions acting on these objects, which are used as callbacks in a set of UI-rendered buttons (see relevant code below).
Two of these buttons are supposed to toggle the boolean state is3D
using the useState hook (in the updateSpatial
function) in order to enable/disable one of these buttons. However, this update obviously causes the component to re-render entirely, thus re-instantiating my classes, which prevent the defined functions to work afterwards.
In contrast, I also tried the useRef hook, which allows for is3D
update without re-rendering, but the button's disabled state is not updated as the component does not re-render.
Is there a pattern that fits with this situation? My next attempts include using HOC, Context, or Redux, but I'm not sure this is the most straighforward. Thanks!
import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";
const Eagles = () => {
const [is3D, setIs3D] = useState(false);
useEffect(() => {
// connect players to destination, ready to play
connectBase();
});
// Instantiating players
const player1= new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
"autostart": false,
"onload": console.log("player 1 ready")
});
const player2 = new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
"autostart": false,
"onload": console.log("player 2 ready")
});
// Functions
function connectBase() {
player1.disconnect();
player1.connect(Destination);
player2.disconnect();
player2.chain(Destination);
}
function playStudio() {
player1.volume.value = 0;
player2.volume.value = -60;
}
function playCinema() {
player1.volume.value = -60;
player2.volume.value = 0;
}
function playstereo() {
player1.volume.value = 0;
player2.volume.value = -60;
updateSpatial();
}
function playmyhifi() {
player1.volume.value = -60;
player2.volume.value = 0;
updateSpatial();
}
function start() {
player1.start();
player2.start();
playstereo();
}
function stop() {
player1.stop();
player2.stop();
console.log("stop pressed")
}
// Update state to toggle button enabled`
function updateSpatial() {
setIs3D((is3D) => !is3D);
}
return (
<Fragment>
<ButtonTL onClick={start}>Play</ButtonTL>
<ButtonTR onClick={stop}>Stop</ButtonTR>
<ButtonTL2 onClick={playstereo}>Stereo</ButtonTL2>
<ButtonTR2 onClick={playmyhifi}>3D</ButtonTR2>
<ButtonLL disabled={is3D} onClick={playStudio}>Studio</ButtonLL>
<ButtonLR onClick={playCinema}>Cinema</ButtonLR>
</Fragment>
)
}
I see two things wrong with your code. First, everything within the "normal" body of the function will be executed on on every render. Hence, the need for states and hooks. States allow you to keep data between renders, and hooks allow you to do a particular action upon a state change.
Second,
useEffect(()=>console.log(hi))
does not have any dependencies, hence it will run on every render.useEffect(()=>console.log(hi),[])
will execute only on the first render.useEffect(()=>console.log(hi),[player1])
will execute when player1 changes.useEffect(()=>console.log(hi),[player1, player2])
will execute when player1 OR player2 change.Be careful with hooks with dependencies. If you set the state of one of the dependencies within the hook itself, it will create an infinite loop
Here is something closer to what you want: