---
# System prepended metadata

title: React 動手做
tags: ['Javascript, React']

---

#  React 動手做

###### tags: `Javascript, React`

# 學習指南：React 介紹

## 使用 react 製作一個簡單的圈圈叉叉小遊戲來學習 react

點擊畫面之後，右邊會記錄下每一步動作

![](https://i.imgur.com/zxrexp7.png)


[成品連結](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)

![](https://i.imgur.com/CC71svQ.png)


 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>
    );
  }
}
```

![](https://i.imgur.com/BrHPFRe.png)


### 建立互動式的 Component

這邊簡單對 button 做 onclick 來做互動，點擊時產生 alert clicked

```javascript=
class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={()=>{
          alert("clicked")
        }}>
        {this.props.value}
      </button>
    );
  }
}
```

![](https://i.imgur.com/DpcVhUb.png)

#### 需要注意的點

把 `() =>` 寫成 `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)
    } />;
  }
```

![](https://i.imgur.com/Bii1auN.png)



#### 現在我們從 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,
    });
  }
```

![](https://i.imgur.com/fyqQnbk.png)


#### 修改 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>
    );
  }
}
```