How do you update the Parent State using a Child Component (Simon Game)?

How do you update the Parent State using a Child Component (Simon Game)?
0
#1

I’m working on the Simon Game project and having trouble updating the parent state via a method defined on the child component.

Here is the pen:
https://codepen.io/codemamba/pen/gJPepG?editors=0010

The child component has this method defined.

  chooseRandomColor() {
    let randomColor = this.props.colors[Math.floor(Math.random()*4)]; //4 bc 4 colors
    alert(randomColor);
    
    this.pressColor(randomColor);
    **this.props.handleStateChange(randomColor)**
  }

This method works fine except for the last line where it calls this.props.handleStateChange.

this.props.handleStateChange(randomColor) is the method that should update the parent state.
It’s supposed to use props to pass the randomColor variable into a handleStateChange function defined on the parent state.

The parent component looks like this:

class SimonGame extends React.Component {
 constructor(props) {
   super(props);
    this.state = {
      colors: ['red', 'green', 'yellow', 'blue'],
      **storedMoves: [],**
      userMoves: []
    }
   this.handleStateChange = this.handleStateChange.bind(this)
 }
  
 **handleStateChange(randomColor)** {
    alert('changing the state ' + randomColor);
    this.setState({ 
      storedMoves: [...storedMoves, randomColor]
    })
 }

  render() {
    return (
      <SimonCircle colors={this.state.colors} handleStateChange={this.handleStateChange} />
    )
  }
}

So chooseRandomColor runs when Start is clicked, and it’s supposed to call this.props.handleStateChange(randomColor) which should run handleStateChange(randomColor) as defined on the parent state, but it’s not working. No alert occurs nor does it update the storedMoves array in State.

Do you have any idea why?

#2

You need to look at the browser console. It is telling you about an error you need to resolve. Once you fix that, you will get a new error which you will need to resolve.

#3
you also do not need to use this.handleStateChange = this.handleStateChange.bind(this) if you change you function to an arrow function like

const handleStateChange = (randomColor) => {
    alert('changing the state ' + randomColor);
    this.setState({ 
      storedMoves: [...storedMoves, randomColor]
    })
 }
#4

You should not use const here.

#5

Thanks for the tip! I didn’t think of that since I’m still new to React. Went into Debug mode and fixed the two errors. Can’t believe I forgot to prefix storedMoves with this.state. It works now. :slight_smile:

#6

is it considered better practice to do this as opposed to bind(this)?

#7

FYI - In React, it is not best practice to manipulate and reference the DOM directly like you are doing in the following:

  pressColor(randomColor) {
    if (randomColor == "red") {
      var pressedColor = document.getElementById("red-circle");
    }
    if (randomColor == "green") {
      var pressedColor = document.getElementById("green-circle");
    }
    if (randomColor == "blue") {
      var pressedColor = document.getElementById("blue-circle");
    }
    if (randomColor == "yellow") {
      var pressedColor = document.getElementById("yellow-circle");
    }
    pressedColor.classList.add("pressed-circle");
    var clearRandomColor = setTimeout(function() {
      pressedColor.classList.remove("pressed-circle");
    }, 500);
  }

Instead, you should only change applicable state properties and let React handle the changes to the DOM. One possible way is to create a state property named pressedColor in SimonGame which would hold a string color name (i.e. ‘red’) representing the button to add the pressed-circle class to. Then, you could pass this value to the ColorButton. Also, I would pass a color prop which represents the color that the button should actually be. Then, inside ColorButon, you would conditionally render pressed-circle class in the className.

#8

Thanks, I decided to change the structure based on your advice.

Could you take a look at the new version?

https://codepen.io/codemamba/pen/rgxPXb?editors=0010

#9

@codemamba Congratulations! This latest version is much more “React-like” than your previous version. You simplify it a bit more by making all but the SimonGame component Stateless Functional Components. Also, you could move the id syntax into the ColorButton component based on the color prop passed into it. This coupled with moving the conditional rendering to be inside ColorButton removes the duplicate code you had in the the SimonCircle component.

Lastly, there was no real need to pass the colors to create a separate chooseRandomColor method in the SimonCircle because you can simply generate the randomColor inside handleStateChange.

const ColorButton = function(props) {
  const pressed = props.colorToPress === props.color ? " pressed-circle" : "";
  return (
    <div id={props.color + "-circle"} className={"inner-circle" + pressed} />
  );
};

const ControlsCircle = function(props) {
  return (
    <div id={props.id}>
      <p onClick={props.onClick}>Start</p>
      <p id="counter">--</p>
      <p>Strict</p>
    </div>
  );
};

//renders the overall model
//also responsible for starting the game
const SimonCircle = function(props) {
  return (
    <div id="outer-circle">
      <ColorButton color="green" colorToPress={props.colorToPress} />
      <ColorButton color="red" colorToPress={props.colorToPress} />

      <ControlsCircle onClick={props.handleStateChange} id="middle-circle" />

      <ColorButton color="yellow" colorToPress={props.colorToPress} />
      <ColorButton color="blue" colorToPress={props.colorToPress} />
    </div>
  );
};

class SimonGame extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      colors: ["red", "green", "yellow", "blue"],
      storedMoves: [],
      userMoves: [],
      colorToPress: ""
    };
    this.handleStateChange = this.handleStateChange.bind(this);
  }

  handleStateChange() {
    var randomColor = this.state.colors[Math.floor(Math.random() * 4)]; //4 bc 4 colors
    this.setState({
      storedMoves: [...this.state.storedMoves, randomColor],
      colorToPress: randomColor //store the current color to press
    });
    var clearColor = setTimeout(() => {
      this.setState({ colorToPress: "" });
    }, 500);
  }

  render() {
    return (
      <SimonCircle
        handleStateChange={this.handleStateChange}
        colorToPress={this.state.colorToPress}
      />
    );
  }
}

ReactDOM.render(<SimonGame />, document.getElementById("app"));
#10

Thanks so much for the help, it’s been really helpful to a React beginner like myself. I’m currently working on figuring out the rest of the game. Hope you don’t mind me asking for help if I run into other issues.

#11

There are many members of the forum who can answer your React questions in case I am not online for some reason.

#12

Thanks again for helping me get started.

I’ve finished the game, and re-designed it. Could you take a look at it?

https://codepen.io/codemamba/pen/VOKgYX

#13

One thing I notice about the game is that it does not give alert you to the fact that you have gotten a sequence wrong until you have pressed the same number of buttons as the level you are on. For example, if I were on level 5 and the following was the simon sequence: Blue, Blue, Yellow, Red, Green and the first button I press is Red, it should go ahead and restart the correct sequence and not wait until I press 4 more buttons.

Other things I noticed:

#1) To make your code a bit more readable, don’t do the following:

let resetColor = setTimeout( () => {this.setState({activeColor: '', readyForUserInput: true,});}, 100) ;

Instead, use indentation:

let resetColor = setTimeout(() => {
  this.setState({
    activeColor: '',
    readyForUserInput: true,
  });
}, 100);

#2) You should use destructuring to make your code cleaner (i.e. avoid write this.state over and over).

  toggleStrictMode() {
    //User can only toggle if game has not begun
    if (this.state.gameInProgress === false) {
      if (this.state.strictMode === false) {
        this.setState({
          strictMode: true,
        })
      }
      if (this.state.strictMode) {
        this.setState({
          strictMode: false,
        })
      }  
    }
  }

becomes:

  toggleStrictMode() {
    const { gameInProgress, strictMode } = this.state;
    //User can only toggle if game has not begun
    if (gameInProgress === false) {
      if (strictMode === false) {
        this.setState({
          strictMode: true,
        })
      }
      if (strictMode) {
        this.setState({
          strictMode: false,
        })
      }  
    }
  }

However, technically the above could be greatly simplified by just writing:

  toggleStrictMode() {
    const { gameInProgress, strictMode } = this.state;
    //User can only toggle if game has not begun
    if (!gameInProgress) {
      this.setState({
        strictMode: !strictMode
      });
    }
  }

#3) I recommend not having more than one nested ternary statement. You pass the following className prop to the SimonCircle component:

        className={
          this.state.strictMode && !this.state.gameInProgress ? 'strict-enabled' 
          : this.state.strictMode && this.state.gameInProgress ? 'strict-enabled pointer-events-disabled' 
          : !this.state.strictMode && this.state.gameInProgress ? 'strict-disabled pointer-events-disabled' 
          : 'strict-disabled'}

That is very complicated and not extremely readable.

#4) I would move all of conditional rendering inside the SimonCircle component. This means you would delete the className prop here and pass two props (strictMode and gameInProgress) to the SimonCircle component.

#14

One thing I notice about the game is that it does not give alert you to the fact that you have gotten a sequence wrong until you have pressed the same number of buttons as the level you are on. For example, if I were on level 5 and the following was the simon sequence: Blue, Blue, Yellow, Red, Green and the first button I press is Red, it should go ahead and restart the correct sequence and not wait until I press 4 more buttons.

Ah, good catch. I thought the game was supposed to wait for the user’s turn to be over before checking. It currently only checks after each click in Strict mode.

#2) You should use destructuring to make your code cleaner (i.e. avoid write this.state over and over)

Does destructuring have to be done within each method? Or can you declare it once for an entire component.

For example, can you declare const {readyForUserInput, strictMode} = this.state; for an entire component such that all methods can access them, or do you have to declare it each time within each method?

#3) I recommend not having more than one nested ternary statement. You pass the following className prop to the SimonCircle component:

What would be an alternate way to write it? I couldn’t figure out if there was a way around nesting ternary operators. IF/IElse If statements can’t be used in this case right.