An Absolute Beginner Learns React, Part III

Andrew (he/him) - Apr 7 '19 - - Dev Community

This is a continutation of my stream of consciousness blog posts on learning React for the first time. I'm working my way through ReactJS.org's tutorial and last time, I made progress toward building a basic tic-tac-toe game. In this blog post, I'll finish it! (Hopefully!)


So when we left off last time, I had just coded the ability for the user to select squares. But they could only make squares into 'X'-es and there was no mechanism for anyone to win. Clearly we have a lot left to do:

Okay, so... what? This text is a bit confusing. I think it's saying that we don't want the board to have to constantly query each square for its state in order to determine if anyone has won the game. It sounds like the squares will send their state to the board when they update (which should only happen once) and the board will keep track of it from that point on. But, like I said, I'm not sure, because this text isn't very clear.

So, the title of this section is "Lifting State" and this is the next block of text I see:

I have to read it a few times to parse it but it sounds like what it's saying is that, whenever you want two components to talk to each other, they must do so through a parent component. I'm not sure why.

...or is this text (and the previous text) saying that it's a recommended practice to do it this way? Is it because any child can pass its state to its parent, and any parent can set the state of the children, but children can't talk to other children through the parent? Is that why "lifting state up" into the parent is encouraged?

A bit of explanation here would be really helpful.

I add this constructor into the Board to initialise the state of the board to nine empty squares:

  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null)
    };
  }
Enter fullscreen mode Exit fullscreen mode

Although, again, in the example code, there's a dangling comma at the end of the line which begins squares: Array.... I remove this dangling comma which I believe is a typo.

The syntax to initialise the this.state.squares is similar to the syntax used to initialise this.state.value in an individual square:

    this.state = {
      value: null
    };
Enter fullscreen mode Exit fullscreen mode
    this.state = {
      squares: Array(9).fill(null)
    };
Enter fullscreen mode Exit fullscreen mode

...except this time, instead of a single value in a single Square, we have an Array of 9 values, each of which we set to null by default. I assume.

I didn't even realise that's what was happening, but I see it now, yeah. Here:

  renderSquare(i) {
    return <Square value={i} />;
  }
Enter fullscreen mode Exit fullscreen mode

...when we render a square, we send it the value i, which is determined by its position in the grid:

        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
Enter fullscreen mode Exit fullscreen mode

So i = 1, 2, 3, .... But the actual render() method within the Square class is:

  render() {
    return (
      <button className="square"
        onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }
Enter fullscreen mode Exit fullscreen mode

It completely ignores the i passed to it, which becomes an unused part of its state:

  constructor(props) {
    super(props);
    this.state = {
      value: null
    };
  }
Enter fullscreen mode Exit fullscreen mode

...and sets the value to X with this.setState({value: 'X'})}, no matter the value that's passed to it. Presumably, next, we'll fix this behaviour and allow the state to be set to X or O, depending on the value passed to renderSquare().

Since we've defined the state of the board in Board.state.squares (which we'll update in the future), we can instead pass a square's state (from that array) to the square by altering the renderSquare() method:

  renderSquare(i) {
    return <Square value={i} />;
  }
Enter fullscreen mode Exit fullscreen mode

becomes

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }
Enter fullscreen mode Exit fullscreen mode

Wow, this section feels a lot longer than the other ones...

Okay, so now that the state of the game is held in Board, any particular Square cannot update the game state directly, as objects cannot directly edit the state of other objects. This next part is a bit complex.

First, if the Squares are no longer keeping track of the game's state, we can delete the constructor entirely, as all it did was set the state of that Square:

class Square extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      value: null
    };
  }

  render() {
    return (
      <button className="square"
        onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

becomes

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

Then, we will pass a function from Board to Square which tells the Square how to handle a click, so

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }
Enter fullscreen mode Exit fullscreen mode

becomes

  renderSquare(i) {
    return (
      <Square
        value   = {this.state.squares[i]}
        onClick = {() => this.handleClick(i)}
      />
    );
  }
Enter fullscreen mode Exit fullscreen mode

Lines are indented for legibility and return must now have a () after it, surrounding its contents. Otherwise, JavaScript's automatic semicolon insertion could break the code. (Who thought that was a good idea?)

This means, of course, that Square should be updated as well. Instead of this.setState({value: 'X'})}, we should use this.props.onClick() in the button's onClick definition:

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

becomes

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.props.onClick()>
        {this.state.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

Oh, and (of course), this.state.value should change to this.props.value as the state of this Square will be sent from the Board to this Square in its props:

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.props.onClick()>
        {this.state.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

becomes

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.props.onClick()>
        {this.props.value}
      </button>
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

I still don't understand how this is all going to come together, but I guess that explanation is on its way.

Oh, yeah, look, there it is. I again run npm start in the terminal and wait an excruciatingly long time for the code to run. (Does anyone else have this problem?) And when it does, I get an error page in the browser:

What did I do?

Oh it looks like I forgot to update {this.state.value} to {this.props.value} in my code, even though I wrote it here. Let's change that and try again:

Great, it worked! It was supposed to crash in that specific way, because we haven't yet defined the onClick() function in this.props.

Also, I'm reading a note in the tutorial and it looks like I've mis-named this function:

So where I have this.props.onClick(), I should change to this.props.handleClick(). Let me reproduce the entire index.js file here for clarity:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {

  render() {
    return (
      <button className="square"
        onClick={() => this.props.handleClick()}>
        {this.props.value}
      </button>
    );
  }

}

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null)
    };
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />;
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

I missed a few other things in the code, as well. Taking notes here and editing the code in the terminal while reading along with the tutorial can be a bit confusing. I think everything above is as it is in the tutorial to this point, so let's continue.

To get rid of that second error ("_this.props.onClick is not a function") and remembering that we renamed onClick to handleClick, we must now define a handleClick method in Board:

I'm not sure I'm really learning anything from this tutorial. More just copying-and-pasting prewritten code. I'll stick through to the end, though.

Within Board, we now define the handleClick() method:

handleClick(i) {
  const squares = this.state.squares.slice();
  squares[i] = 'X';
  this.setState({squares: squares});
}
Enter fullscreen mode Exit fullscreen mode

Before I read ahead, let me see if I can guess what this is doing. First, it's a function that takes a single parameter i, which is the index of the square on the board (either 0-8 or 1-9, I don't know if JavaScript is 0-based or 1-based). It then creates a constant local variable within the method which it initialises to its own state.squares. I have no idea why slice() needs to appear there if squares is already an array. Also, why is squares declared as const when we change the value of one of its elements in the very next line? Finally, we set the state with setState. It looks like variables are passed by value in JavaScript, so we have to explicitly copy the value of squares.state into a local variable, which we edit, then pass that edited variable back to change the state. How much of that is right?

...okay, I'll learn about this later, I guess.

It's literally the next paragraph that begins to explain this. Why even have that "we'll explain this later" if you're going to talk about it in the next breath? Here's why they suggest doing it the way they did:

The way that was natural to me was to edit the state of the Square directly, but the way that the tutorial recommends is to create a new object and not to mutate the existing one. The tutorial recommends keeping objects immutable as much as possible so that changes are easy to detect and the application can easily be reverted to a previous state, among other benefits.

Jeez. Okay. Maybe it's me, because I don't have a really strong JavaScript / reactive front-end programming background, but this tutorial seems to jump from one concept to another with basically no segue. There doesn't really seem to be a clear goal or learning pathway in mind. I feel like I'm just learning a random concept, then copying some code, then moving on to the next thing. Not a big fan so far.

Okay, that does actually seem easier. Since the square holds no state itself, it will be rendered by calling this function from within Board.

Okay but why. No explanation given, we move on to the next thing.

Before we change the renderSquare() function in Board, we're going to add the ability to draw Os on the board, as well as Xes. We set the initial state in the Board's constructor:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null)
    };
  }
Enter fullscreen mode Exit fullscreen mode

becomes

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true
    };
  }
Enter fullscreen mode Exit fullscreen mode

And again, there is a dangling comma at the end of xIsNext: true, which I have removed. Is this intentional?

So xIsNext is a boolean that we'll flip each time we render a square. When we rewrite renderSquare() (I suppose) we'll flip xIsNext from false to true or vice versa, and check the status of xIsNext before we decide to draw an X or an O. We change

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }
Enter fullscreen mode Exit fullscreen mode

to

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext
    });
  }
Enter fullscreen mode Exit fullscreen mode

(Again, removing a dangling comma.)

Oops, typo. Let me fix that.

Getting there! The game still doesn't declare a winner, as you can see above. I guess that's the next thing to do, but before we do that, the tutorial wants us to render a message saying who's turn it is. We add the line:

    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
Enter fullscreen mode Exit fullscreen mode

...to the top of the Board's render() function (right now, it always says X is the next player):

I just noticed, too, that when I edit the index.js file, React automatically re-renders the page at localhost:3000. That's pretty neat!

Okay, last things last: how do we declare a winner?

I'm really running out of steam at this point so I'm glad that this section is almost over.

Definitely not a fan of the style of this tutorial. 0/10. Would not recommend.

I would prefer a tutorial that starts with the smallest understandable bits of code and works up from there, rather than starting with a skeleton and saying "okay, now copy and paste the contents of this to there" over and over. Ugh.

After mindlessly copying some more code...

...it works! But I'm not happy about it.


There's one more section in this tutorial but I am seriously lacking the motivation to complete it. I think I'd like to try a different tutorial or book that starts from the basics and builds on them.

I'm quitting this tutorial 75% of the way through. I feel frustrated and I don't feel like I actually learned much about React. Maybe the people at ReactJS.org should consider doing some focus group testing for this tutorial because I'm sure I'm not the only person who has had this reaction.

In the comments on one of my previous posts, Kay Plößer recommended their book React From Zero which sounds like it might be a bit more up my alley. I think I'll give React a second chance after I take some time to recover from this experience.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .