简体   繁体   中英

Access and change the state array element in main React App from grandchild

I apologize for the headline of this question, but I tried to explain the problem as much as I could in one line.

Our school assignment required us to create a (sort of) Tic Tac Toe game but in a certain way. Instead of creating nine different Box elements from the main App , we were required to create Row class and Box class, so the main App will have three Row class children and each Row class will have three Box children.

Now the problem I encountered is how to change the main App state array at a particular element that corresponds to that exact Box component.

I am a beginner and I apologize in advance if I am not seeing something too obvious. If you have any questions, please do ask, I'll be more than happy to explain this as much as I can. Thank you!

    import React, { Component } from 'react';
    import { render } from 'react-dom';
    
    class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          0 : ['-','-','-'],
          1 : ['-','-','-'],
          2 : ['-','-','-']
        };
        this.handleClick = this.handleClick.bind(this)
      }
      handleClick(){
        // if that particular element is '-' change from '-' to 'X'
        // else if it is 'X' change to 'O'
        // else change to 'X'
      }
      render() {
    
        const rows = []
        for (let i = 0; i < 3; i++) {
          rows.push(<Row key = {i} msg = {this.state[i]} handleClick = {this.handleClick}/>)
        }
    
        return (
          <div>
            <div>
              <h1>Tic Tac Toe</h1>
            </div>
            <div id = 'main'>
              {rows}
            </div>
          </div>
        );
      }
    }
    class Row extends Component{
    
      render() {
        const boxes = []
        for (let i = 0; i < 3; i++) {
          boxes.push(<Box key = {i} msg = {this.props.msg[i]} handleClick = {this.props.handleClick}/>)
        }
        return (
          <div className = 'row'>
            {boxes}
          </div>
        )
      }
    
    }
    
    class Box extends Component{
    
      render() {
        return (
          <div className = 'box' onClick = {this.props.handleClick}>
            <p>{this.props.msg}</p>
          </div>
        )
      }
    
    }
    render(<App />, document.querySelector('#root'));

You have organized your components well. You need to handle player switching after each click and updating the values in the game board.

  1. Add another state variable called player (it can just be a boolean as this is a two-player game)
this.state = {
      0: ["-", "-", "-"],
      1: ["-", "-", "-"],
      2: ["-", "-", "-"],
      player: true
    };
  1. You can pass row and col index to handleClick function like below.
  handleClick(row, col) {
    // set the corresponding value (using row and colum index) in the game borad
    this.setState(
      {
        [row]: this.state[row].map((val, colId) =>
          colId === col ? (this.state.player ? "X" : "O") : val
        )
      },
      () => {
        // switch the player (toggle the player boolean value)
        this.setState({ player: !this.state.player });
      }
    );
  }
  1. Pass the row and col ids from the click handler in Box component.
       <Box
          ...
          ...
          handleClick={() => {
            this.props.handleClick(this.props.row, i);
          }}
        />

Code Sandbox

NOTE: when you develop further to avoid changing the same Box by clicking, you can keep an object to store more info like { value: "-", used: false } instead of "-" as values.

Building off of @Amila Senadheera's answer, you could also add a check in the handleClick function to ensure that a player can't overwrite another player's move. Something along the lines of:

handleClick(row, col) {
    // set the corresponding value (using row and colum index) in the game board
    if(this.state[row][col] !== '-') {
      return // Don't allow the player to overwrite an already-marked square.
    }
    this.setState(
      {
        [row]: this.state[row].map((val, colId) =>
          colId === col ? (this.state.player ? "X" : "O") : val
        )
      },
      () => {
        // switch the player (toggle the player boolean value)
        this.setState({ player: !this.state.player });
      }
    );
  }

And if you want to add a check for victory, there's some useful discussion here: https://codereview.stackexchange.com/questions/24764/tic-tac-toe-victory-check .

You could define parameters for handleClick like

type T = 1 | 2 | 3;
function handleClick(row: T, col: T) {
  const newRow = [ ...this.state[row] ];
  this.sign = this.sign === "O" ? "X" : "O";
  newRow[col] = this.sign;
  this.setState({...this.state, [row]: newRow});
}

and then you need new props for Box:

for (let i = 0; i < 3; i++) {
  boxes.push(
    <Box 
      row={props.row}
      col={i}
      key={i} 
      msg={this.props.msg[i]} 
      handleClick={this.props.handleClick}/>
  )
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM