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)
};
}
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
};
this.state = {
squares: Array(9).fill(null)
};
...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} />;
}
...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>
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>
);
}
It completely ignores the i
passed to it, which becomes an unused part of its state
:
constructor(props) {
super(props);
this.state = {
value: null
};
}
...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} />;
}
becomes
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}
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 Square
s 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>
);
}
}
becomes
class Square extends React.Component {
render() {
return (
<button className="square"
onClick={() => this.setState({value: 'X'})}>
{this.state.value}
</button>
);
}
}
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]} />;
}
becomes
renderSquare(i) {
return (
<Square
value = {this.state.squares[i]}
onClick = {() => this.handleClick(i)}
/>
);
}
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>
);
}
}
becomes
class Square extends React.Component {
render() {
return (
<button className="square"
onClick={() => this.props.onClick()>
{this.state.value}
</button>
);
}
}
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>
);
}
}
becomes
class Square extends React.Component {
render() {
return (
<button className="square"
onClick={() => this.props.onClick()>
{this.props.value}
</button>
);
}
}
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')
);
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});
}
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 const
ant 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 O
s on the board, as well as X
es. 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)
};
}
becomes
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true
};
}
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});
}
to
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext
});
}
(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');
...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.