Updating state without rendering the whole React component (useState)

4.1k Views Asked by At

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>
  )
}
1

There are 1 best solutions below

2
On BEST ANSWER

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:

import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";

const Eagles = () => {

  const [is3D, setIs3D] = useState(false);
  const [player1]= useState(new Player({
    "url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
    "autostart": false,
    "onload": console.log("player 1 ready")    
  }));
  const [player2] = useState(new Player({
    "url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
    "autostart": false,
    "onload": console.log("player 2 ready")
  }));   

  useEffect(() => {
    // connect players to destination, ready to play
    connectBase();
  },[player1, player2]);
  
  // Instantiating players
            
  // 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>
  )
}