Search code examples
javascriptreactjsreact-hooksuse-effectuse-state

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?


Solution

  • 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.