Search code examples
reactjsreact-statereact-class-based-component

creating a piano with react.js, use of state unclear


I'm about to build up a keyboard for a piano and I want to use state in order handel the pressed and non pressed keys (keys that finally generate the sound, so that I can play single notes and chords)

I modified an online tutorial the following way: First of all I created these objects:

export const NOTES = [
  { name: "c", accid: "natural", hz: 3270, keyboard: "q" },
  { name: "d", accid: "flat", hz: 3465, keyboard: "2" },
  { name: "d", accid: "natural", hz: 3671, keyboard: "w" },
  { name: "e", accid: "flat", hz: 3889, keyboard: "3" },
  { name: "e", accid: "natural", hz: 4120, keyboard: "e" },
  { name: "f", accid: "natural", hz: 4365, keyboard: "r" },
  { name: "g", accid: "flat", hz: 4625, keyboard: "5" },
  { name: "g", accid: "natural", hz: 4900, keyboard: "t" },
  { name: "a", accid: "flat", hz: 5191, keyboard: "6" },
  { name: "a", accid: "natural", hz: 5500, keyboard: "z" },
  { name: "b", accid: "flat", hz: 5827, keyboard: "7" },
  { name: "b", accid: "natural", hz: 6174, keyboard: "u" },
];

My first step is to load the pressed keyboard keys into an array that is hooked up to the global state object. Later on I want to create a sound for all the objects represented by the keyboard keys which are represented in that state array. But first things first.

Why is this.state.pressedKeys (spread operator) undefined, when I press a key?

TypeError: Cannot read property 'pressedKeys' of undefined

import React from "react";
import Key from "./Key";
import "./Piano.css";
import { NOTES } from "../global/constants";

class Piano extends React.Component {
  constructor(props) {
    super(props);
    this.state = { pressedKeys: [] };
  }

  componentDidMount = () => {
    window.addEventListener("keydown", this.handleKeyDown);
  //  window.addEventListener("keyup", this.handleKeyUp);
  };

  handleKeyDown(event) {
    if (event.repeat) {
      return;
    }

    const key = event.key;
    const updatedPressedKeys = [...this.state.pressedKeys];
    if (!updatedPressedKeys.includes(key)) {
      updatedPressedKeys.push(key);
    }

    this.setState({
      pressedKeys: updatedPressedKeys,
    });
  }

  render() {
    const keys = NOTES.map((note) => {
      return (
        <Key
          key={note.hz}
          note={note.name}
          accid={note.accid}
          pressedKeys={this.state.pressedKeys}
        />
      );
    });

    return <div className="piano">{keys}</div>;
  }
}

export default Piano;

Solution

  • You've missed binding the this of the class component to the handleKeyDown callback, so this isn't referring to the component. This results in this.state being undefined in the callback.

    In order of preference:

    1. You can bind using arrow function.

      handleKeyDown = event => { .... }
      
    2. You can bind in the constructor.

      constructor(props) {
        super(props);
        this.state = { pressedKeys: [] };
      
        this.handleKeyDown = this.handleKeyDown.bind(this);
      }
      
    3. You can bind when attaching/passing prop.

      window.addEventListener("keydown", this.handleKeyDown.bind(this));
      

    I suggest also using a functional state update to compute if key is included and create the next state if necessary.

    handleKeyDown = event => {
      if (event.repeat) {
        return;
      }
    
      const { key } = event;
    
      this.setState(prevState => {
        if (prevState.pressedKeys.includes(key)) {
          return prevState;
        }
        return {
          pressedKeys: [...prevState.pressedKeys, key],
        }
      });
    }