Drum Machine / Cannot update a component (`App`) while rendering a different component (`Pad`)

Tell us what’s happening:
I’m trying to tackle the Drum Machine challenge with React. It was pretty easy to deal with the click on pads, but much tougher to handle the keyPress.
I did something that “works”, but I ended up with this error message in the console:

Warning: Cannot update a component (App) while rendering a different component (Pad). To locate the bad setState() call inside Pad, follow the stack trace as described in Bug: too hard to fix "Cannot update a component from inside the function body of a different component." · Issue #18178 · facebook/react · GitHub

While it doesn’t prevent the code from running, it still looks pretty ugly. I’ve identified that it’s this logic in my component that is creating the issue, but I don’t see any other options at the moment

    if (keyPressed===Number(keyCode)) {
        setKeyPressed("");
        const audio = new Audio(sound);
        audio.play();
        setDisplay(id);
    };

I understand is a bit long to review it and I hope my message is making sense. In any case, many thanks for checking this out and happy coding everyone!!

Your code so far

App.js

import './App.css';

import {useState, useEffect} from "react";

import sound1 from "./assets/sound1.mp3";
import sound2 from "./assets/sound2.mp3";
import sound3 from "./assets/sound3.mp3";
import sound4 from "./assets/sound4.mp3";
import sound5 from "./assets/sound5.mp3";
import sound6 from "./assets/sound6.mp3";
import sound7 from "./assets/sound7.mp3";
import Pad from './components/pad';

function App() {
  const [display, setDisplay] = useState("Play your sound");
  const [keyPressed, setKeyPressed] = useState("");
  useEffect(() => {
    const handleKeyDown = (e) => {
      setKeyPressed(e.keyCode);
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, []);
  return (
    <div className="container">
      <div id="drum-machine">
        <div id="display">{display}</div>
        <div id="drum-pads">
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Work it" keyCode="65" sound={sound1} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Make it" keyCode="90" sound={sound2} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Do it" keyCode="69" sound={sound3} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Makes us" keyCode="82" sound={sound4} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Harder" keyCode="81" sound={sound5} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Better" keyCode="83" sound={sound6} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Faster" keyCode="68" sound={sound7} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Stronger" keyCode="70" sound={sound7} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="More than" keyCode="87" sound={sound7} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Faster" keyCode="88" sound={sound7} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="Stronger" keyCode="67" sound={sound7} />
          <Pad setDisplay={setDisplay} keyPressed={keyPressed} setKeyPressed={setKeyPressed} id="More than" keyCode="86" sound={sound7} />
        </div>
      </div>
    </div>
  );
}

export default App;

pad.js

const Pad = ({setDisplay, keyPressed, setKeyPressed, id, keyCode, sound}) => {
    const handleClick = (e) => {
        e.target.children[0].play();
        setDisplay(id);
    };
    if (keyPressed===Number(keyCode)) {
        setKeyPressed("");
        const audio = new Audio(sound);
        audio.play();
        setDisplay(id);
    };
    return (
        <button onClick={handleClick} id={id} className="drum-pad">
        {id}
        <audio src={sound}></audio>
        </button>
    )
};

export default Pad;

Your browser information:

User Agent is: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36

Challenge: Build a Drum Machine

Link to the challenge:

It shouldn’t work with the Audio constructor anyway. The test uses properties on the audio element (calls .play() and checks .paused).

You can look at the event.key to see what key was pressed and use that to get to the correct audio element.

It would be nice with a GitHub repo or a live version on StackBlitz (or CodeSandbox, however it seems to have issues with our test script last I checked). The Vite React template on StackBlitz should work with the test (just add the script to the HTML).

Thanks a lot for checking this out, much appreciated!

I understand that the use of the audio constructor will prevent me from passing test and that I should use DOM to get access to the relevant pad and run play() on them. I guess I can try that, maybe with getElementById after changing the various props.

However, even with that, I’d still be modifying the state of the app component (display and keyPressed) from within the body of the pad component. Which is resulting in the following error message in the console:

Warning: Cannot update a component ( App ) while rendering a different component ( Pad ). To locate the bad setState() call inside Pad , follow the stack trace as described in Bug: too hard to fix “Cannot update a component from inside the function body of a different component.” · Issue #18178 · facebook/react · GitHub

But I got no clue where to put / how to trigger the logic if a key is pressed.

Also I put it all in a public github repo - GitHub - JossMilon/fcc-drum-machine: 3rd exercice to pass the front end libraries certification - https://www.freecodecamp.org/learn/front-end-development-libraries/

Don’t pass down and call setDisplay directly inside the child. Create a handler function in the parent that calls setDisplay and pass down the handler. The same goes for setKeyPressed although I’m not really sure what you are using that for.

Parent:

const handleSetDisplay = (id) => {
  setDisplay(id);
};

<Pad setDisplay={handleSetDisplay}  id="someId"/>

Child:

setDisplay(id);

You should read the requirements for User Story #3 and #4 again as well. What you are doing now will not work with the requirements. And just as an aside, id attribute values should not contain whitespace.

Thanks for the help mate, put me on what I thin k is the right track. eventually I migrated all the logic with keyPress to the app component using a switch. Probably lots of optimization required, but I’m still a js/react noob so got to progress there and at least, I don’t have console error anymore!

Good catch for id! and will rework #3 and #4!!

Happy coding and thanks for helping me out!!