Tutorial Demo
# Tutorial Demo
This section is based on the official Tutorial (opens new window).
Online demo (opens new window)
# Demo Code
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// Square (can use a function component when there's no state)
function Square (props) {
// return returns a React element
return (
// <button> is a built-in component, onClick is its built-in click handler prop
<button
className="square"
onClick={props.onClick}
>
{props.value}
</button>
)
}
// Board component (collection of squares)
class Board extends React.Component {
renderSquare (i) {
// Parentheses prevent automatic semicolon insertion from breaking the structure
return (
// Pass two props to the Square component: value and onClick
// Note: onClick here passes a prop, it doesn't bind a click event directly
// Event naming convention: on[Event]={ this.handle[Event] }
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
)
}
// The render method describes what you want to see on screen
render () {
return (
<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>
)
}
}
// Game component (outermost layer)
class Game extends React.Component {
constructor(props) {
// super is required in JavaScript class inheritance constructors
super(props)
// state is private to the current component
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0, // The history step currently being viewed
xIsNext: true,
}
}
// Handle square click (triggered by child component)
handleClick (i) {
// The slice here discards "future" history when going "back in time", and setState will erase the original "future" records.
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1]
const squares = current.squares.slice() // slice() creates an array copy
if (calculateWinner(squares) || squares[i]) { // If there's a winner or the square is already taken
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O'
this.setState({
// concat doesn't mutate the original array, while push does
history: history.concat([{
squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
})
}
// Jump to a history step
jumpTo (step) {
// history is not modified here
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
})
}
render () {
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares)
// History steps (map transforms the original array into another array)
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
)
})
let status
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
// Note: onClick here passes a prop, it doesn't bind a click event directly
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
)
}
}
ReactDOM.render(
<Game />,
document.getElementById('root')
)
// Calculate winner
function calculateWinner (squares) {
// Winning combinations
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
// Check if values in any winning combination are the same, and return the winner
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# Why Immutability Is Important in React
In the demo, when modifying state, we don't directly modify object properties. Instead, we first create a copy of the array using .slice(), then replace the entire array via setState().
Generally, there are two approaches to changing data. The first is to directly mutate the data by changing its values. The second is to replace the data with a new copy.
# Direct Data Mutation
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// player is now {score: 2, name: 'Jeff'}
2
3
# Replacing Data with a New Copy
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}
// Using object spread syntax:
// var newPlayer = {...player, score: 2};
2
3
4
5
6
7
Not mutating (or changing the underlying data) achieves the same result, with several benefits:
# Simplifying Complex Features
Immutability makes complex features much easier to implement. Later in the tutorial, we will implement a "time travel" feature that allows us to review the tic-tac-toe game's history and "jump back" to previous moves. This functionality isn't unique to games -- an ability to undo and redo certain actions is a common requirement in applications. Avoiding direct data mutation lets us keep previous versions of the game's history intact, and reuse them later.
# Detecting Changes
Detecting changes in mutable objects is difficult because they are modified directly. This detection requires the mutable object to be compared to previous copies of itself and the entire object tree to be traversed.
Detecting changes in immutable objects is considerably easier. If the immutable object that is being referenced is different than the previous one, then the object has changed.
#
# Determining When to Re-Render in React
The main benefit of immutability is that it helps you build pure components in React. Immutable data can easily determine if changes have been made, which helps to determine when a component requires re-rendering.
You can learn more about shouldComponentUpdate() and how you can build pure components by reading Optimizing Performance (opens new window).