Lursmani
Lursmani

Reputation: 169

React hooks - how to force useEffect to run when state changes to the same value?

So I'm building a drum-pad type of app, and almost everything is working, except this.

Edit: Put the whole thing on codesandbox, if anyone wants to have a look: codesandbox.io/s/sleepy-darwin-jc9b5?file=/src/App.js

const [index, setIndex] = useState(0);
const [text, setText] = useState("");
const [theSound] = useSound(drumBank[index].url)   

function playThis(num) {
  setIndex(num)
}

useEffect(()=>{
  if (index !== null) {
    setText(drumBank[index].id);
    theSound(index);
    console.log(index)
  }
}, [index])

When I press a button, the index changes to the value associated with the button and then the useEffect hook plays the sound from an array at that index. However, when I press the same button more than once, it only plays once, because useState doesn't re-render the app when the index is changed to the same value.

I want to be able to press the same button multiple times and get the app to re-render, and therefore useEffect to run every time I press the button. Can anyone help me how to do this?

Upvotes: 2

Views: 10234

Answers (2)

Nat Wallbank
Nat Wallbank

Reputation: 1457

Have you considered combining those discrete state variables (value types) into a single reference type state object?

Instead of having an effect that sets the text when the index changes you just set the entire state at the same time.

As long as you ensure this is a new object/reference then the effect will fire. The effect is then only responsible for playing the sound based on the current state.

Here's some sample code

const [state, setState] = useState({ index: 0, text: '', }); 
const playThis = (num) => { 
    setState({ index: num, text: drumBank[num].id, }); 
}; 
useEffect(() => { 
    theSound(state.index); 
}, [state]);

Upvotes: 2

Drew Reese
Drew Reese

Reputation: 202618

Here's what I could come up with from your sandbox.

  1. According to the docs each useSound is just a single sound, so when trying to update an index into a soundbank to use via React state the sound played will always be at least one render cycle delayed. I suggest creating a new custom hook to encapsulate your 9 drum sounds.

    useDrumBank consumes the drumbank array and instantiates the 9 drum sounds into an array.

    const useDrumBank = (drumbank) => {
      const [drum0] = useSound(drumbank[0].url);
      const [drum1] = useSound(drumbank[1].url);
      const [drum2] = useSound(drumbank[2].url);
      const [drum3] = useSound(drumbank[3].url);
      const [drum4] = useSound(drumbank[4].url);
      const [drum5] = useSound(drumbank[5].url);
      const [drum6] = useSound(drumbank[6].url);
      const [drum7] = useSound(drumbank[7].url);
      const [drum8] = useSound(drumbank[8].url);
    
      return [drum0, drum1, drum2, drum3, drum4, drum5, drum6, drum7, drum8];
    };
    
  2. Update the component logic to pass the drumBank array to the new custom hook.

    const sounds = useDrumBank(drumBank);
    

Here's the full code:

function App() {
  useEffect(() => {
    document.addEventListener("keypress", key);

    return () => document.removeEventListener("keypress", key);
  }, []);

  const [text, setText] = useState("");
  const sounds = useDrumBank(drumBank);

  function playThis(index) {
    drumBank[index]?.id && setText(drumBank[index].id);
    sounds[index]();
  }

  function key(e) {
    const index = drumBank.findIndex((drum) => drum.keyTrigger === e.key);
    index !== -1 && playThis(index);
  }

  return (
    <div id="drum-machine" className="drumpad-container">
      <div id="display" className="drumpad-display">
        <p>{text}</p>
      </div>
      <button className="drum-pad" id="drum-pad-1" onClick={() => playThis(0)}>
        Q
      </button>
      <button className="drum-pad" id="drum-pad-2" onClick={() => playThis(1)}>
        W
      </button>
      <button className="drum-pad" id="drum-pad-3" onClick={() => playThis(2)}>
        E
      </button>
      <button className="drum-pad" id="drum-pad-4" onClick={() => playThis(3)}>
        A
      </button>
      <button className="drum-pad" id="drum-pad-5" onClick={() => playThis(4)}>
        S
      </button>
      <button className="drum-pad" id="drum-pad-6" onClick={() => playThis(5)}>
        D
      </button>
      <button className="drum-pad" id="drum-pad-7" onClick={() => playThis(6)}>
        Z
      </button>
      <button className="drum-pad" id="drum-pad-8" onClick={() => playThis(7)}>
        X
      </button>
      <button className="drum-pad" id="drum-pad-9" onClick={() => playThis(8)}>
        C
      </button>
    </div>
  );
}

Demo

Edit react-hooks-how-to-force-useeffect-to-run-when-state-changes-to-the-same-value

Usage Notes

No sounds immediately after load

For the user's sake, browsers don't allow websites to produce sound until the user has interacted with them (eg. by clicking on something). No sound will be produced until the user clicks, taps, or triggers something.

Getting the keypresses to consistently work seems to be an issue and I don't immediately have a solution in mind, but at least at this point the button clicks work and the sounds are played synchronously.

Upvotes: 2

Related Questions