# React 動手做
###### tags: `Javascript, React`
# 學習指南:React 介紹
## 使用 react 製作一個簡單的圈圈叉叉小遊戲來學習 react
點擊畫面之後,右邊會記錄下每一步動作

[成品連結](https://codepen.io/gaearon/pen/gWWZgR?editors=001)
可以先在連結處操作一下,以方便理解待會程式碼的邏輯
## React 是什麼?
> React 是一個陳述式、高效且具有彈性的 JavaScript 函式庫,用以建立使用者介面。
>
### React.component
```javascript=
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
```
從上方程式碼可以看到
* render
* class
* React.component
* 一些 html 的 tag (JSX)
我們可以這樣理解這個 component
1. 這邊的 shoppingList 是一個 React 的 component
1. component 會接受 prop (properties) 在大括號的部分
1. 並且會透過 render 函式回傳 view (React element)到螢幕上
### JSX
這邊的 html tag 使用的是 JSX 的寫法,基本上跟下方程式碼是一樣的,但是明顯比 JSX 還要難以使用,因此被 babel 編譯過後就是 JSX 的寫法,也就大部分跟 html tag 的寫法相似
JSX 就跟 JavaScript 一樣強大。你可以在 JSX 中的括號中放入任何 JavaScript 的表達式。每個 React element 都是一個 JavaScript 的 object,你可以把它存在一個變數中或在程式中互相傳遞。
[完整版程式碼](https://babeljs.io/repl/#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=DwEwlgbgBAxgNgQwM5IHIILYFMC8AiJACwHsAHUsAOwHMBaOMJAFzwD4AoKKYQgRlYDKJclWpQAMoyZQAZsQBOUAN6l5ZJADpKmLAF9gAej4cuwAK5wTXbg1YBJSswTV5mQ7c7XgtgOqEETEgAguTuYFamtgDyMBZmSGFWhhYchuAQrADc7EA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.14.5&externalPlugins=)
```javascript=
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
```
## 開發者工具
使用 chrome 可以使用此工具讓你檢查你的 React component 中的 props 和 state。
[連結](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)

CodePen 中使用開發者工具需要一些額外的步驟:
1. 登入網站,或註冊並確認你的 email (為防止垃圾郵件的必要手續)。
1. 點擊 Fork 按鈕。
1. 點擊 Change View 並選擇 Debug mode。
1. 在新開啟的分頁中,devtools 現在應該有 React 的 tab 了。
## 遊戲製作開始
[起頭檔案在此CSS已經寫好](https://codepen.io/gaearon/pen/oWWQNa?editors=0010)
可以看到 react 部分有三個 component
* Square 主要目的在於 render 出九宮格點擊的功能
* Board 主要目的在於 render 出九宮格內部的數字
* Game 主要處理遊戲棋盤以及右側的遊戲資訊
### 透過 Prop 傳遞資料
> 傳遞 prop 是 React 的應用程式中資訊從 parent 傳給 children 的方式。
透過 Board 的 value 放入 i 來當作 props 傳遞
```javascript=
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
}
```
這邊使用大括號`{this.props.value}`來接收 props 並且 render 到 九宮格 內部
```javascript=
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
```

### 建立互動式的 Component
這邊簡單對 button 做 onclick 來做互動,點擊時產生 alert clicked
```javascript=
class Square extends React.Component {
render() {
return (
<button className="square" onClick={()=>{
alert("clicked")
}}>
{this.props.value}
</button>
);
}
}
```

#### 需要注意的點
把 `() =>` 寫成 `onClick={alert('click')}` 是一個常見的錯誤,這會造成 component 在每次重新 render 時都會觸發 alert ,而不是點擊數字觸發,要特別注意。
### 使用 state
實作點擊之後出現X記號 Component 使用 state 來保持狀態。
1. 我們要先加一個 constructor 在 class 中以初始化 state
2. 用 super 呼叫父類別的 props 來做繼承
3. 修改 onClick 內容 使用 setState 給 state 初始值
4. 修改 button 內容抓取 state 的內容填進 button 也就是 'X'
所以數字消失,變成當九宮格內容被點擊時,會立刻把 X 傳進去九宮格
```javascript=
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>
);
}
}
}
```
## 完成遊戲
### 提升 State
> 最好的方式是把這整個遊戲的 state 存放在 Board parent component 中,而不是在每一個 Square 中(之後會再提升到game去)
現在要操作把 square 中的 state 提升到 parent component 中也就是 board component 內
1. 在 board 內新增 constructor (刪掉 square 內的)
2. 將 board 內的 state 設置唯一個 array 充滿了 null
3. 在 renderSquare 內部改寫 square 內容填入 state 記憶的內容並根據格數提取
4. 在寫入一個 onClick 來處理點擊事件由 board 傳回給 square(當 square 被點擊時觸發)
所以從這邊開始 state 的狀態就可以記錄在 board 內,並且藉由 props 的方式傳遞給 squares 應該顯示的內容
```javascript=
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)
} />;
}
```

#### 現在我們從 Board 傳兩個 prop 給 Square
* value (圈或叉)
* onClick 事件(當框框被點擊時觸發)
#### 這邊要針對 Square 做出修改
1. 因為 state 被拉到 board 所以要把 this.state.value 替換成 this.props.value
2. this.setState() 替換成 this.props.onClick()
3. 把 constructor 從 Square 中刪除,因為 Square 已不再需要追蹤遊戲的狀態。
現在的 square 看起來像這樣
```javascript=
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
```
#### 理解一下 onClick 如何做到
* onClick 被寫在 button 時,react 就會被告知要設定一個 click event listener
* 當按鈕被點擊,react 會呼叫 square 內的 onClick 也就是 `this.props.onClick()`
* 這時 square 內的`this.props.onClick()`會被board指定
* 因為是由 board 傳送 這個 props 給 square 使用的
* 因此這個 props 的內容是 `this.handleClick(i)` 也就是最後 square 會執行的內容
#### 定義 handleClick()
這邊是 onClick 會觸發的函式目前會寫在 board 內部
```javascript=
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
```
### Function Component
> 在 React 中,當我們要寫只包含 render 方法且沒有自己 state 的 component 時,function component 是一種很簡易的寫法。
由於 square 的 state 被放到 board 去了所以可以用 function component 的方式做改寫
```javascript=
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
```
### 輪流玩遊戲(圈圈要登場了)
首先針對 state 的部分給他加上布林值
```javascript=
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
```
接下來針對點擊事件處理
* 加上圓圈,並且透過三元判斷子來判斷圈或叉
* 每次點擊修改布林值就可以達到點擊一次叉下一次就會是圈出現
```javascript=
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
```

#### 修改 board 中顯示下一位玩家的部分成動態顯示
```javascript=
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// 以下不需改變
```
[現在你的程式碼應該會長這樣](https://codepen.io/gaearon/pen/KmmrBy?editors=0010)
### 決定勝負
由下面這個函式來決定勝者
1. 傳入變數 squares 也就是九宮格內部的內容(圈叉)
1. 在 lines 中填入所有的勝利組合的陣列
2. 使用 for 迴圈印出所有勝利組合,並且使用 if 判斷是否 squares 目前的內容有匹配的勝利組合,如果沒有則繼續迴圈直到勝利組合出現,不然就是 return null 繼續 show 出 Next player 字樣
這邊解釋一下 if 內的判斷式
* square[a] 的值基本上一定為 true
* 其他的 `a===b a===c` 都為 true 時表示它們都是圈或都是叉這時候就可以 return squares 也就是勝者出爐了
* 如果`a===b a===c`不為 true 則繼續跑下個迴圈
```javascript=
function calculateWinner(squares) {
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],
];
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;
}
```
下方程式碼在處理顯示贏家以及下一位參賽者的字樣顯示
* 透過 if 判斷式處理如果 winner 有值則回傳 winner
* 沒有的 winner 的值出現則顯示下一位玩家
```javascript=
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// 以下不需改變
```
#### 勝負揭曉終止程式
在 handleClick 的部分處理如果
使用 if 判斷式
* 如果 贏家出現有值就為 true 則函式就會 return 終止
* 如果還是空的則此段程式碼不執行下方程式碼繼續跑
```javascript=
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
```
[目前程式碼已經可以玩小遊戲了](https://codepen.io/gaearon/pen/LyyXgK?editors=0010)
### 再次提升 State
這次要把 state 提升到 game 內,為了可以讀取所有的步驟
```javascript=
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
```
下一步讓 board 可以接收來自 game 的 squares, onClick 的 props
```javascript=
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
```
這時候把 handleClick 整個搬過來所以 board 的部份的就刪除掉
因為要加入過去的動作所以設定 history , current
```javascript=
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null)
}],
xIsNext: true
};
}
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
xIsNext: !this.state.xIsNext,
});
}
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
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>{/* TODO */}</ol>
</div>
</div>
);
}
}
```