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