React 是由 Facebook 所主導開發的 JavaScript 框架,與 AngularJS 相同都是採用組件 Componet-based 來進行觀念導向設計,不像 VueJS 採用 MVVM(Model 資料管理、View 畫面顯示、ViewModal 溝通橋梁)觀念去區分細節,而是整個融合在 Componet 整個零件內。
React React 的官方教學其實十分完善充裕並提供中文,本篇內容將跟隨官方的手冊摸索進行學習,正式介紹前以下有幾個 React 特性可以討稐。
組件導向 React 凡事都採用 Componet 來製作組件,需要就可重複運用。且都主要依賴 JavaScript 來編寫,因此整體維護性質很高。
只有 View 由於不是 MVC 觀念,只注重在顯示 View 這部分。因此較能輕易跟其他框架工具混合使用。
以 JSX 來表示 HTML 由於 React 主要環境以 JavaScript 來編寫,輸出 HTML 元素相對麻煩些(你需要先 create Element 然後 insert 等過程), 因此 React 建議(非絕對)可考慮使用 JSX 技術來完成這部分。JSX 是指透過標籤方式直接寫在 JavaScript 內,當 React 進行 Complier 時透過 Babel 編譯成一般的 JavaScript 語法提供給瀏覽器,Babel 是一種轉譯語言(與 TypeScript 為同類型轉譯工具),Babel 主要用途除了 JSX 這種快速編寫 HTML 的技術,另外也提供像是 JS 編譯降版相容等功能。
Virtual DOM 渲染 React 不會直接操作網頁實體 DOM,而是透過自己的虛擬化 DOM 來進行渲染化。虛擬化 DOM 只會針對局部已改變的應用套在實體 DOM,這能加快效能速度。
Hook 功能 在 React 16.8 版本開始使用,可以在不使用 JavaScript 的 Class(舊方法)就能方便的使用 React 功能(未來主流方向)。官方手冊目前仍是以 Class 方式來呼喚 React 功能,而 Hook 是另外篇幅介紹。本站也會另外介紹 Hook,在工作上兩套都還是要理解,看公司 React 環境是否引入 Hook 製成為主。
安裝 檔案可以主要有 2 筆 React 檔案,以及額外的 1 筆 Babel 檔案(如果你要用 JSX 來編寫 HTML 則需用到)。React 檔案除了主檔案react.js
之外,還有一隻負責 Virtual DOM 的react-dom.js
。本節的 React 語法可以看不懂先無視,主要是測試 React 安裝是否可運作。而你開始接觸 JS 框架這個程度上,你很清楚在專案上的安裝使用可以分很多方法來完成。React 不例外也分為以下方法使用:
透過 CDN 想直接使用可透過 CDN 來引入,提供了未壓縮(開發用)跟已壓縮兩種來源。
使用 CDN 存取 React 建議 script 標籤屬性添加 crossorigin 確保跨網域存取能正常。
<script crossorigin src ="https://unpkg.com/react@17/umd/react.development.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@17/umd/react-dom.development.js" > </script > <script crossorigin src ="https://unpkg.com/react@17/umd/react.production.min.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" > </script >
不使用 JSX 的寫法 這裡提供不想使用 Babel 來編寫 React 的寫法,事實上複雜度較高。
index.html <body > <div id ="btnLoki" > </div > <script crossorigin src ="https://unpkg.com/react@17/umd/react.development.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@17/umd/react-dom.development.js" > </script > <script src ="custom.js" > </script > </body >
接著在設計 component 當下,需要透過 React 來 createElement 創造 HTML 元素,以及對這個元素進行 HTML 編寫。最後透過虛擬 DOM 指定哪個元素跟組件並渲染到哪個實體 DOM。
custom.js 'use strict' ;const btn = React .createElement ; class LikeButton extends React.Component { constructor (props ) { super (props); this .state = { liked : false }; } render ( ) { if (this .state .liked ) { return 'You liked this.' ; } return btn ( 'button' , { onClick : () => this .setState ({ liked : true }) }, 'Like' ); } } const domContainer = document .querySelector ('#btnLoki' );ReactDOM .render (btn (LikeButton ), domContainer);
使用 JSX 的寫法 如果透過 Babel 的 JSX 來設計,就會非常簡單。但要多引用 BabelJS 以及讓 Babel 知道哪個 JS 檔案需要做 JSX 轉譯。
index.html <body > <div id ="btnLoki" > </div > <script crossorigin src ="https://unpkg.com/react@17/umd/react.development.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@17/umd/react-dom.development.js" > </script > <script src ="https://unpkg.com/babel-standalone@6/babel.min.js" > </script > <script type ="text/babel" src ="custom.js" > </script > </body >
設計組件時,就不用 React 的 createElement,直接將 JSX 寫在組件內即可。而虛擬 DOM 當下只需要直接寫組件並渲染到實體 DOM 上,因為 HTML 元素透過 JSX 直接寫在組件內。
custom.js 'use strict' ;class LikeButton extends React.Component { constructor (props ) { super (props); this .state = { liked : false }; } render ( ) { if (this .state .liked ) { return 'You liked this.' ; } return ( <button onClick ={() => this.setState({ liked: true })}> Like </button > ); } } const domContainer = document .querySelector ('#btnLoki' );ReactDOM .render (<LikeButton /> , domContainer);
透過 CLI 所謂的 CLI 是提供一個系統輔助工具,能夠快速建立 React 開發所需要的伺服器環境與轉譯系統。React 提供了Create React App
的 Node 工具(可另簡稱為React cli
)。能無腦的解決環境應用進行開發,推薦新手學習使用。Create React App 本身僅包含了 webpack 與 babel 的前端建置管道,不提供任何後端服務功能。
node 環境下輸入指令進行安裝,並指定一個應用 APP 名稱建立node.js npx create-react-app my-app
npx 為 npm 5.2+ 提供的打包 CLI 工具,能確保原本該全域安裝的 node 套件被限定在該專案目錄下,如此一來就可以讓本機環境乾淨些。否則以 npm 安裝需要寫成npm install -g create-react-app
並透過指令create-react-app my-app
來建立應用 APP 名稱。
檢查專案內會多一個 my-app 的應用目錄,以及在 package.json 內可以看到以提供 start 指令。我們將位置切換至應用 APP 位置並啟用網站服務。node.js
此時會獲得一個網站網址為http://localhost:3000/
,並且已經存在一個 DEMO 用的專案應用網站。透過這個 DEMO 網站進行內部檔案簡單擺放說明(規則是 webpack 所規定的,當然也可異動):
public: 作為公共的檔案區,舉例看到 index.html 或圖片放置在這裡。
src: 為 React 程式集中區,包含 React 會 import 的 css 也放在這,舉例看到 index.js 與 App.js 主要兩隻檔案放在這裡。
build: 如果要將專案發佈到線上環境,透過npm run build
進行建置作業,會產生該目錄(產生瀏覽器看得懂的純 HTML、JS、CSS 專案目錄)。
擴展開發工具 如果希望能在開發上獲得更好的檢查工具,可透過加裝 Chrome 擴充工具 ,其他品牌瀏覽器可 參考官方推薦 。
初次運行 開始進入基本介紹 React 相關基礎知識。本篇整體將採用 CLI 方式進行練習測試,這能簡化 HTML 編寫且省去宣告 Babel 動作。練習方式請從前面介紹Create React App
所提供的 DEMO 包開始動作, src 內部所有檔案並將練習的 React 檔案從src/index.js
開始編寫,調整 html 請從public/index.html
開始改寫。初始動作準備如下:
public/index.html <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <div id ="root" > </div > </body > </html >
要使用 React 之前,必須先從專案目錄內匯入 React 與 ReactDOM 才能正常使用 React 與 DOM 渲染。
src/index.js import React from 'react' ; import ReactDOM from 'react-dom' ; ReactDOM .render ( <h1 > Hello, world!</h1 > , document .getElementById ('root' ) );
簡單示意對 div 元素 DOM 編寫內容 h1 標籤為 hello world。
JSX 語法 JSX 如前面已介紹,能夠在 JavaScript 內直接寫入 HTML 標籤,使得透過 Babel 偵測自動編譯成 JavaScript。例如下面寫法直接編寫。另外若需夾任何表達式都能以{}
包覆,例如變數、運算符、帶回傳之函式等各種。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;const name = 'Josh Perez' ;const element = <h1 > Hello, {name}</h1 > ; ReactDOM .render ( element, document .getElementById ('root' ) );
由於 JSX 屬於 JavaScript,因此在屬性上寫法採用駝峰式命名需注意大小寫。同時因為 class 為特殊關鍵字須改用 className。
const element1 = <h1 className ="cls" tabIndex ="0" > Class 名稱特別要改寫</h1 > ;const element2 = <h1 tabIndex ="0" > 一些屬性要改駝峰式寫法</h1 > ;const element3 = <img src ={user.img} /> ;
也能用()
來包覆一個複合 HTML。
const element = ( <div > <h1 > Hello!</h1 > <h2 > Good to see you here.</h2 > </div > );
JSX 預設只能對應一個 HTML 元素,如果你想對應多個元素,除非像上面用 div 包起來當作一個,否則需要透過空元素來包覆。
const el = ( <> <button > 123</button > </> );
Render 渲染 React 的元素使用 JSX 來編寫形成一個物件,而瀏覽器的元素 node 是實體 DOM。如想要將元素物件塞到實體 DOM 做成 UI 畫面,就必須透過ReactDOM.js
的特定函式ReactDOM.render(ReactElement,DOM_NODE)
來達到渲染。這個函式本身只會執行一次,一次將所有的虛擬 DOM 需求渲染成實體 DOM,這能保證每次渲染只會更新必要的元素替換。
public/index.html <body > <div id ="root" > </div > </body >
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;const element = <h1 > Hello, world</h1 > ;ReactDOM .render (element, document .getElementById ('root' ));
每次 React 的元素物件是不可變的,如果要重複做 UI 輸出,就必須要再做一個新元素物件提交至ReactDOM.render()
。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function tick ( ) { const element = ( <div > <h1 > Hello, world!</h1 > <h2 > It is {new Date().toLocaleTimeString()}.</h2 > </div > ); ReactDOM .render (element, document .getElementById ('root' )); } setInterval (tick, 1000 );
Componet 組件 由於 React 採用組件導向的框架,設計方式就是先建立組件接著利用組件來顯示頁面結果。定義組件的作法基礎方式就是採用純 JavaScript 函式來構成,而 React 的組件是採 ES6 的 class 來定義(透過繼承 React.Componet 獲得原型),不過最近新觀念是使用 Hook,未來另起篇幅介紹。
function Welcome (props ) { return <h1 > Hello, {props.name}</h1 > ; } class Welcome extends React.Component { render ( ) { return <h1 > Hello, {this.props.name}</h1 > ; } }
以上的 function componet 與 class componet 在 React 的上獲得是相同的內容,而參數 props 為傳遞資料可做額外的資訊輸出組合。
設計 Componet 組件命名時必需一定是大寫開頭,這是避免在 JSX 內編寫時與小寫開頭的 HTML 元素混淆衝突,例如<Div />
與<div />
前者是組件後者是 HTML 標籤。
渲染輸出 ReactDOM.render() 將組件輸出到畫面上的做法如前面提到的,透過ReactDOM.render(組件物件,目標 NODE)
來完成,組件的物件寫法為<NAME/>
來代表。
public/index.html <body > <div id ="demo1" > </div > <div id ="demo2" > </div > </body >
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function WelcomeF ( ) { return <h1 > Hello!!</h1 > ; } const elementF = <WelcomeF /> ;ReactDOM .render (elementF, document .querySelector ('#demo1' ));class WelcomeC extends React.Component { render ( ) { return <h1 > Hello!!</h1 > ; } } const elementC = <WelcomeC /> ;ReactDOM .render (elementC, document .querySelector ('#demo2' ));
偷懶技巧 如果寫膩太長可以這樣省略寫法。前提是用途單純只有React.Component
與ReactDOM.render
。
src/index.js import {Component } from 'react' ; import {render} from 'react-dom' ;class WelcomeC extends Component { render ( ) { return <h1 > Hello!!</h1 > ; } } const elementC = <WelcomeC /> ;render (elementC, document .querySelector ('#demo2' ));
傳遞資料 props 如果這個組件有提供屬性參數會以 props 物件方式來保留,用途廣泛的傳遞資料於內外存取屬性。例如我們對外部組件設定添加myname=Loki
,便能在內部組件利用此 props 來獲得進行應用。舉例使用 JSX 的{}
表達式來插入 props 物件之變數:
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function WelcomeF (props ) { return <h1 > Hello, {props.myname}!!</h1 > ; } const elementF = <WelcomeF myname ="Loki" /> ;ReactDOM .render (elementF, document .querySelector ('#demo1' ));class WelcomeC extends React.Component { render ( ) { return <h1 > Hello, {this.props.name}!!{this.</h1 > ; } } const elementC = <WelcomeC name ="Loki" /> ;ReactDOM .render (elementC, document .querySelector ('#demo2' ));
如果是 function 寫法則在 props 此傳遞變數上來使用。如果是 class 寫法則透過 this 底下的 props 進行取得。
props 本身就是一種函數內的傳遞值因此是不可改的(無法在組件內試圖修改 props),React 遵循 Pure function 觀念只能對 props 限制僅讀取用。如果需要雙向更新的用法可改用 State 方式 ( 唯獨 class 而 function 沒有 )。
組合 Component 小組件本身可重複利用在另一個大組件內利用 JSX 編寫重複使用,再透過 props 獲得差異性的顯示。首先這裡創造一個 MyApp 作為新組件並嘗試在 JSX 上使用 Welcome 組件。
src/index.json import React from 'react' ;import ReactDOM from 'react-dom' ;class Welcome extends React.Component { render ( ) { return <h1 > Hello, {this.props.name}!!</h1 > ; } } class MyApp extends React.Component { render ( ) { return ( <div > <Welcome name ="Loki" /> <Welcome name ="Max" /> <Welcome name ="July" /> </div > ); } } ReactDOM .render ( <MyApp name ="Loki" /> , document .querySelector ('#demo1' ) );
通常 React 會用這樣的方式在最外層自訂命名 app 代表整個組合式 Componet,將類似 Button 這樣的小組件寫在一起。
分解 Componet 試著將下面單一式組件改成組合式組件。如果需要可偷偷增加console.log
來檢查this.props
內容物。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function formatDate (date ) { return date.toLocaleDateString (); } const comment = { date : new Date (), text : 'I hope you enjoy learning React!' , author : { name : 'Hello Kitty' , avatarUrl : 'https://placekitten.com/g/64/64' , }, }; class Comment extends React.Component { render ( ) { return ( <div className ="Comment" > <div className ="UserInfo" > <img className ="Avatar" src ={this.props.author.avatarUrl} alt ={this.props.author.name} /> <div className ="UserInfo-name" > {this.props.author.name} </div > </div > <div className ="Comment-text" > {this.props.text} </div > <div className ="Comment-date" > {formatDate(this.props.date)} </div > </div > ); } } ReactDOM .render ( <Comment date ={comment.date} text ={comment.text} author ={comment.author} /> , document .getElementById ('demo1' ) );
首先最小單位的開始處理,整體最內部的img.Avatar
下手。抽取出來獨立一個 Avatar 組件,而原本 Comment 組件的該處改成組件標籤。這裡故意使用 user 代表為 Comment 提供給 Avatar 專用的 props 變數獨立名稱不受影響。src/index.js class Avatar extends React.Component { render ( ) { console .log ('Avatar' , this .props ); return ( <img className ="Avatar" src ={this.props.user.avatarUrl} alt ={this.props.user.name} /> ); } } class Comment extends React.Component { render ( ) { console .log ('Comment' , this .props ); return ( <div className ="Comment" > <div className ="UserInfo" > {/* <img className ="Avatar" src ={this.props.author.avatarUrl} alt ={this.props.author.name} /> */} <Avatar user ={this.props.author} /> {/* ... */} </div > {/* ... */} </div > ) } }
在 Avatar 的上層為 UserInfo,也進行進行抽離並將原本的 Avatar 組件一同搬移至另一組件內,開始變成組合式組件。scr/index.js class Avatar extends React.Component { render ( ) { console .log ('Avatar' , this .props ); return ( <img className ="Avatar" src ={this.props.user.avatarUrl} alt ={this.props.user.name} /> ); } } class UserInfo extends React.Component { render ( ) { console .log ('UserInfo' , this .props ); return ( <div className ="UserInfo" > <Avatar user ={this.props.author} /> <div className ="UserInfo-name" > {this.props.author.name} </div > </div > ); } } class Comment extends React.Component { render ( ) { console .log ('Comment' , this .props ); return ( <div className ="Comment" > <UserInfo author ={this.props.author} /> <div className ="Comment-text" > {this.props.text} </div > <div className ="Comment-date" > {formatDate(this.props.date)} </div > </div > ); } }
完成後整體代碼如下,抽離成組合式組件在大型應用上常見到,但不是什麼都要抽離成組件。經驗上若有些 UI 會重複使用或是結構本身過於複雜,組合式組件下重複利用是很好的組合方式。src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function formatDate (date ) { return date.toLocaleDateString (); } const comment = { date : new Date (), text : 'I hope you enjoy learning React!' , author : { name : 'Hello Kitty' , avatarUrl : 'https://placekitten.com/g/64/64' , }, }; class Avatar extends React.Component { render ( ) { return ( <img className ="Avatar" src ={this.props.user.avatarUrl} alt ={this.props.user.name} /> ); } } class UserInfo extends React.Component { render ( ) { return ( <div className ="UserInfo" > <Avatar user ={this.props.author} /> {/* 引用 Avatar 組件 */} <div className ="UserInfo-name" > {this.props.author.name} </div > </div > ); } } class Comment extends React.Component { render ( ) { return ( <div className ="Comment" > <UserInfo author ={this.props.author} /> {/* 引用 UserInfo 組件 */} <div className ="Comment-text" > {this.props.text} </div > <div className ="Comment-date" > {formatDate(this.props.date)} </div > </div > ); } } ReactDOM .render ( <Comment date ={comment.date} text ={comment.text} author ={comment.author} /> , document .getElementById ('demo1' ) );
Function 轉 Class 如果選擇請盡量使用 Class 方式來建立組件,只有 Class 才能提供 State 與生命週期這方面的特性。欲將 Function 轉為 Class 有以下步驟:
建立一個相同名稱並且繼承 React.Component 的 ES6 class。
加入一個 render() 的空方法。
將 function 的內容搬到 render() 方法。
將 render() 內的 props 替換成 this.props。
刪除剩下空的 function 宣告。
前面幾例介紹已將官方手冊的 function 改寫成 Class 可自參考兩者差異,這裡再追述稍早的範例進行轉換練習。然而這個 tick 函式不算是 React 的組件,僅只是普通函式將 JSX 透過 render() 渲染輸出。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function tick ( ) { const element = ( <div > <h1 > Hello, world!</h1 > <h2 > It is {new Date().toLocaleTimeString()}.</h2 > </div > ); ReactDOM .render (element, document .getElementById ('root' )); } setInterval (tick, 1000 );
先將適合做成的組件建立起來,而時間物件從外面產生透過 props 傳入到組件內。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function Clock (props ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {props.date.toLocaleTimeString()}.</h2 > </div > ); } function tick ( ) { ReactDOM .render (<Clock date ={new Date ()} /> , document .getElementById ('root' )); } setInterval (tick, 1000 );
接著將 Clock 的 function 組件改成 Class 組件。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class Clock extends React.Component { render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.props.date.toLocaleTimeString()}.</h2 > </div > ); }; } function tick ( ) { ReactDOM .render (<Clock date ={new Date ()} /> , document .getElementById ('root' )); } setInterval (tick, 1000 );
像這樣不斷地透過 setInterval 重複呼叫 ReactDOM.render 來刷新網頁。其實可以透過 State 與生命週期來優化。
區間狀態 Local state state 是 Class 型組件才有的特性(另外還有生命週期等特性),前例的 Date 物件 是從 props 傳遞進去不可改,因此每次都要重新執行該組件。這裡由組件內部的 state 變數來負責記錄 Date 物件(才能在組件內進行修改值),另外改變 setInterval 從外部方式 render 方式改由生命週期負責更新。
首先將 JSX 內的 props 改成 state。
組件 class 宣告建構子 constructor 初始化 this.state 為 Date 物件。
因為用到 this 所以需要宣告 super(props),而 props 從 constructor 帶入。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.state.date.toLocaleTimeString()}.</h2 > </div > ); }; } ReactDOM .render (<Clock /> , document .getElementById ('root' ));
React 規定需要透過 super() 參照父類別 React.Component 的建構子,這樣才能使用父類別的 this。而這裏帶入 props 當作參數是確保建構子內配置當下的 this.props 等價外部的 props。雖然這裡沒用到 props 但初學者都要求乖乖寫這兩行確保運作正常。
不使用 constructor 來初始 state 上述定義 class 屬性的方式比較麻煩,所以 ES7 推出了另一個定義方式為 class properties proposal,可以省略 constructor 直接在 class 內定義屬性。後續代碼會跟隨官方教學同樣使用前方式來編寫,後期上手熟悉後可改換此方式來設定 state。
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class Clock extends React.Component { state = { date : new Date () }; render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.state.date.toLocaleTimeString()}.</h2 > </div > ); }; } ReactDOM .render (<Clock /> , document .getElementById ('root' ));
單向資料流 Unidirectional Data flow 組合式組件其各自的組件 state 是各自獨立存在 Local 互不影響,可使用瀑布的觀念透過將來自上層的 state 值,利用 props 傳遞給下層。但注意的是這是單向,你無法將下層的 state 傳遞給上層。範例中將 Clock 分解出一個時間文字格式。原本為將改成這樣:
src/index.js class FormatDate extends React.Component { render ( ) { return <h2 > It is {this.props.date.toLocaleTimeString()}.</h2 > ; } } class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } render ( ) { return ( <div > <h1 > Hello, world!</h1 > <FormatDate date ={this.state.date} /> </div > ); }; } ReactDOM .render (<Clock /> , document .getElementById ('root' ));
改寫 setState() 然而需要對 state 進行改寫時,必需透過this.setState()
對 state 內的資料進行修改。這裡添加一個 tick() 方法使得若在 Class 內使用 tick() 時就能改寫 state。
src/index.js class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.state.date.toLocaleTimeString()}.</h2 > </div > ); }; }
setState 注意事項 由於 setState 本身原理是透過 Marge 方式跟 state 變數進行合併並覆蓋寫回,因此這裡些列出一些 setState 會遇到的基本問題。
setState() 指定進去的通常為一個物件資料。不可以直接寫入,例如this.state.comment = 'Hello';
其 React 並不會更新。正確寫法為this.setState({comment: 'Hello'})
。
由於 React 為了效能在批次動作下只做一次更新,導致 this.props 與 this.state 當下可能是非同步狀態,因此以下額外提供範例有可能無法正確更新this .setState ({ counter : this .state .counter + this .props .increment , });
因此解決方式為,先透過一個具備 return 的函式做讀取後再進行更新。this .setState (function (state, props ) { return { counter : state.counter + props.increment }; }); this .setState ((state, props ) => ({ counter : state.counter + props.increment }));
setState() 的原理是將所提供的物件進行 Marge 合併。因此可以寫入時提供物件格式給予合併使用。例如constructor (props ) { super (props); this .state = { posts : [], comments : [] }; } componentDidMount ( ) { fetchPosts ().then (response => { this .setState ({ posts : response.posts }); }); fetchComments ().then (response => { this .setState ({ comments : response.comments }); }); }
生命週期 Mount 與 Unmount 生命週期是指當一個組件進行 Render 到實體 DOM 上時的掛載 Mount 動作開始,直到這個 DOM 被移除後進行卸載 Unmount 而結束。React 提供了多種週期操作方法,例如componentDidMount()
與componentWillUnmount()
能在觸發掛載與卸載時進行自訂工作。
範例中我們將 setInterVal 寫在componentDidMount()
上,也就是當該組件被掛載到畫面上時會觸發這裡的作業。
componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); }
整理來說由於 State 是雙向動態的,只要 State 有更新會影響 React 偵測到 Render 自動更新的動作,不用再次去手動渲染。可檢查畫面已經可以正常計時並自動更新,比起之前的方法來說這裡的 ReactDOM.render() 只被執行一次,剩下透過 state 變化來雙向調整畫面異動。
這裡多做一個動作是紀錄 interval 的編號,假設某情況需要停止計時需要進行 clearInterval。而this.timeID
與 React 本身建構子資料無關可以由 ES6 觀念自訂來私用。另外多訂一個動作是 5 秒後會移除這個 DOM,透過ReactDOM.unmountComponentAtNode()
完成卸載,與 ReactDOM.render() 是反向的動態 DOM 操作。
componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); setTimeout (() => { ReactDOM .unmountComponentAtNode (document .getElementById ('root' )); }, 5000 ); }
接著可以指定componentWillUnmount()
該做取消 interval 動作,代表當進行 UnMount 時要做的事情,不寫這行會報錯。
componentWillUnmount ( ){ clearInterval (this .timeID ); }
最後,組件是可以重複被引用在大 Class 底下,多增加一個 Class 來重複引用 Clock,整體如下:
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class FormatDate extends React.Component { render ( ) { return <h2 > It is {this.props.date.toLocaleTimeString()}.</h2 > ; } } class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); setTimeout (() => { ReactDOM .unmountComponentAtNode (document .getElementById ('root' )); }, 5000 ); } componentWillUnmount ( ){ clearInterval (this .timeID ); } render ( ) { return ( <div > <h1 > Hello, world!</h1 > <FormatDate date ={this.state.date} /> </div > ); }; } class App extends React.Component { render ( ){ return ( <div > <Clock /> </div > ); } } ReactDOM .render (<App /> , document .getElementById ('root' ));
Events 事件 在 React 內對某 HTML 元素綁訂一個事件,可透過 JSX 來編寫。整體寫法與 HTMLDOM 的事件寫法雷同,但有以下差異性:
React Events 採用駝峰式命名;HTML 的 events 屬性標籤做差異。
綁定事件執行某函式,React 的 events 值只需要寫該函式名稱即可;HTML 則是直接寫字串 (JS 語法)。src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;function demoFn ( ) { console .log ("hello" ); } const el = ( <button onClick ={demoFn} > React Event</button > ) ReactDOM .render (el, document .getElementById ('root' ));
React 不接受 return false 的方式來取消 events(HTML DOM 可以,例如onsumbit="return false"
),因此需透過 preventDefault 來取消預定事件動作。src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class MySubmit extends React.Component { demoFn (e ) { e.preventDefault (); console .log ("Submit!!" ); } render ( ) { return ( <form onSubmit ={this.demoFn} > <button type ="submit" > Submit</button > </form > ); }; } ReactDOM .render (<MySubmit /> , document .getElementById ('root' ));
因此消失的 this 使用 Class 來設計組件,我們會利用 state 來進行動態顯示。前面出現範例,欲對 state 修改是沒有問題的。
class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } }
然而,假設改由事件來處理某個 Class 內的方法,下一步自然會接著由 this 來操作 Class 內的資源,會發現 this 是不存在的。由前面範例調整一個按鈕 events 舉例:
src/index.js import React from 'react' ;import ReactDOM from 'react-dom' ;class FormatDate extends React.Component { render ( ) { return <h2 > It is {this.props.date.toLocaleTimeString()}.</h2 > ; } } class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } stopTime ( ) { clearInterval (this .timeID ); } render ( ) { return ( <div > <FormatDate date ={this.state.date} /> <button onClick ={this.stopTime} > STOP</button > </div > ); }; } class App extends React.Component { render ( ) { return ( <div > <Clock /> </div > ); } } ReactDOM .render (<App /> , document .getElementById ('root' ));
由於 JavaScript 的 this 預設不會特別綁定任何對象,因此 this 會根據函式的 callback 方式不同而決定誰是對象。當函式被某對象進行調用時該 this 能代表該對象。而原本 Class 內部透過 Callback 的方式為setState()
都能將其 this 代表本身 Class。
但是若被 props(指 onClick) 寫在 JSX 內使函式名稱作為值之字串編寫,其 callback 方式為setState
沒有寫上小誇號,無法屆時認定為執行 callback 的對象為何 Class 本身,因此則當作獨立函式進行。
簡單來說,就因為寫法為this.setState
而不是this.setState()
,使得整個訪問對象不是 Class 本身。
解決方式:bind 既然如此強迫初始 constructor 階段下,先將指定函式 bind 綁定塞入 this 物件。如此一來不是透過 () 所 callback 的狀況下,this 很明確的知道是 class 本身。
調整 Class 如下:
class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; this .stopTime = this .stopTime .bind (this ); } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } stopTime ( ) { clearInterval (this .timeID ); } render ( ) { return ( <div > <FormatDate date ={this.state.date} /> <button onClick ={this.stopTime} > STOP</button > </div > ); }; }
解決方式 2: 執行函式 根據官方建議可透過 Public class fields 語法,將 event 執行去處發一個 class 內(既 this) 的箭頭函式內容。
class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } stopTime = () => { clearInterval (this .timeID ); } render ( ) { return ( <div > <FormatDate date ={this.state.date} /> <button onClick ={this.stopTime} > STOP</button > </div > ); }; }
解決方式 3:setState on event 想辦法變成是透過this.setState()
來觸發,因此調整 JSX 的指令,將 event 的值更改為觸發這個句話。
class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } stopTime ( ) { clearInterval (this .timeID ); } render ( ) { return ( <div > <FormatDate date ={this.state.date} /> <button onClick ={() => this.stopTime()}>STOP</button > </div > ); }; }
此寫法若需要多塞指定之參數 (ex:id) 的寫法可以這樣用:
><button onClick ={(e) => this.stopTime(id,e)}>STOP</button >
解決方式 4:bind on event 其實為方法 3 與方法 1 之合併版本,在 JSX 上的函式名稱做bind(this)
。
class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } tick ( ) { this .setState ({ date : new Date () }); } componentDidMount ( ) { this .timeID = setInterval (() => this .tick (), 1000 ); } stopTime ( ) { clearInterval (this .timeID ); } render ( ) { return ( <div > <FormatDate date ={this.state.date} /> <button onClick ={this.stopTime.bind(this)} > STOP</button > </div > ); }; }
此寫法若需要多塞指定之參數 (ex:id) 的寫法可以用
><button onClick ={this.stopTime.bind(this,id)} > STOP</button >
額外範例 另外官方手冊出現的範例說明:
class Toggle extends React.Component { constructor (props ) { super (props); this .state = {isToggleOn : true }; this .handleClick = this .handleClick .bind (this ); } handleClick ( ) { this .setState (prevState => ({ isToggleOn : !prevState.isToggleOn })); } render ( ) { return ( <button onClick ={this.handleClick} > {this.state.isToggleOn ? 'ON' : 'OFF'} </button > ); } } ReactDOM .render ( <Toggle /> , document .getElementById ('root' ) );
流程判斷 由於 React 貼近 JavaScript 的語法操作,因此整合性來說非常直接上手,主要是練習思考哪些場合來使用流程判斷。下例示範內有 2 處用到 if 判斷:
設計中組件(邏輯用)來判斷選擇哪個小組件 (UI 用),其後大總成輸出時,只需使用該組件即可。
先使用變數指定判斷為哪個小組件 (UI 用),其後大總成輸出時組件,只需使用該變數即可。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;function UserGreeting (props ) { return <h1 > Welcome back!</h1 > ; } function GuestGreeting (props ) { return <h1 > Please sign up.</h1 > ; } function Greeting (props ) { const isLoggedIn = props.isLoggedIn ; if (isLoggedIn) { return <UserGreeting /> ; } return <GuestGreeting /> ; } function LoginBtn (props ) { return <button onClick ={props.onClick} > Login</button > ; } function LogoutBtn (props ) { return <button onClick ={props.onClick} > Logout</button > ; } class LoginControl extends Component { constructor (props ) { super (props); this .handleLoginClick = this .handleLoginClick .bind (this ); this .handleLogoutClick = this .handleLogoutClick .bind (this ); this .state = { isLoggedIn : false }; } handleLoginClick ( ) { this .setState ({ isLoggedIn : true }); } handleLogoutClick ( ) { this .setState ({ isLoggedIn : false }); } render ( ) { const isLoggedIn = this .state .isLoggedIn ; let button; if (isLoggedIn) button = <LogoutBtn onClick ={this.handleLogoutClick} /> ; else button = <LoginBtn onClick ={this.handleLoginClick} /> ; return ( <div > <Greeting isLoggedIn ={isLoggedIn} /> {button} </div > ); } } ReactDOM .render ( <LoginControl /> , document .getElementById ('root' ) );
JSX 的邏輯表達式 你可以利用 JSX 的{}
來插入一個複合邏輯並根據特性true && expression
將回傳 expression,而false && expression
則回傳 false 之技巧,做一個簡易具備行內判斷的 JSX。注意:如果 JSX 出現{false}
之表達結果代表忽略。
import ReactDOM from 'react-dom' ;function Mailbox (props ) { const unreadMessages = props.unreadMessages ; return ( <div > <h1 > Hello!</h1 > {unreadMessages.length > 0 && <h2 > You have {unreadMessages.length} unread messages. </h2 > } </div > ); } const messages = ['React' , 'Re: React' , 'Re:Re: React' ];ReactDOM .render ( <Mailbox unreadMessages ={messages} /> , document .getElementById ('root' ) );
但自身要注意複合邏輯的處理特性,例如下例中,因為 count 為 0(認同 false) 導致 count && 後續被忽略,結果為 count。也就是輸出會是<div>0</div>
。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class Demo extends Component { render ( ) { const count = 0 ; return ( <div > {count && <h1 > Messages: {count}</h1 > } </div > ); } } ReactDOM .render (<Demo /> ,document .getElementById ('root' ));
三元運算子 JSX 也可以在{}表達式內去使用三元運算子。隨著複雜程度不同可使用在不同 JSX 環境場合下,以下範例根據前面出現過的代碼重新調整,為 2 處使用到三元運算子:
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;function LogoutButton (props ) { return <button onClick ={props.onClick} > Logout</button > } function LoginButton (props ) { return <button onClick ={props.onClick} > Login</button > } class Demo extends Component { constructor (props ) { super (props); this .state = { isLoggedIn : false }; this .handleLogoutClick = this .handleLogoutClick .bind (this ); this .handleLoginClick = this .handleLoginClick .bind (this ); } handleLogoutClick ( ) { this .setState ({ isLoggedIn : false }); } handleLoginClick ( ) { this .setState ({ isLoggedIn : true }); } render ( ) { const isLoggedIn = this .state .isLoggedIn ; return ( <div > <div > The user is <b > {isLoggedIn ? 'currently' : 'not'}</b > logged in.</div > <div > {isLoggedIn ? <LogoutButton onClick ={this.handleLogoutClick} /> : <LoginButton onClick ={this.handleLoginClick} /> } </div > </div > ); } } ReactDOM .render (<Demo /> , document .getElementById ('root' ));
控制 Component 不輸出渲染 我們可利用 return null 的特性要求組件不進行渲染行為,達到該組件具備何時才會 render 之機制。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;function WarningBanner (props ) { if (!props.warn ) return null ; return ( <div className ="warning" > Warning! </div > ); } class Page extends Component { constructor (props ) { super (props); this .state = { showWarning : true }; this .handleToggleClick = this .handleToggleClick .bind (this ); } handleToggleClick ( ) { this .setState ( state => ( { showWarning : !state.showWarning } ) ); } render ( ) { return ( <div > <WarningBanner warn ={this.state.showWarning} /> <button onClick ={this.handleToggleClick} > {this.state.showWarning ? 'Hide' : 'Show'} </button > </div > ); } } ReactDOM .render ( <Page /> , document .getElementById ('root' ) );
資料陣列 Array Lists 進行資料整理時,React 會經常加以使用 JavaScript 的 Array 原生函式map()
,這是一個可以將陣列循序取出後再回傳至陣列的映射處理。以下為 JavaScript 的範例解釋:
const numbers = [1 , 2 , 3 , 4 , 5 ];const doubled = numbers.map ((number ) => number * 2 );console .log ('doubled' , doubled);
利用此特性,將陣列透過 mapr 收集成 JSX 元素標籤的陣列型態。可發現陣列內容如下:
const numbers = ['Loki' , 2 , 3 , 4 , 5 ];const listItems = numbers.map ((number ) => <li > {number}</li > ); console .log ('listItems' , listItems);
其檢查內容為:
接著,將此 JSX 陣列直接 render 出來,React 處理時會自動將多筆資料進行批次輸出。
src/index.js import ReactDOM from 'react-dom' const numbers = ['Loki' , 2 , 3 , 4 , 5 ];const listItems = numbers.map ((number ) => <li > {number}</li > ); ReactDOM .render ( <ul > {listItems}</ul > , document .getElementById ('root' ) );
列表化組件 將前面的 map 改成都由 Componet 來負責輸出,外面只需要提供列表清單並透過 props 來傳達給組件。
scr/index.js import { Component } from 'react' ;import ReactDOM from 'react-dom' class MyComponet extends Component { constructor (props ) { super (props); this .lists = this .props .nums .map (e => <li > {e}</li > ); } render ( ) { return <ul > {this.lists}</ul > ; } } const numAry = [1 , 2 , 3 ];ReactDOM .render ( <MyComponet nums ={numAry} /> , document .getElementById ('root' ) );
雖然正常顯示,但透過瀏覽器 Console 錯誤訊息會提示你的組件內的子項目資料應該要提供 key 去指定。因此調整一下 map 內的動作提供 key 屬性。但然而這裡因為 key 你根本用不到但必需要存在。
scr/index.js import { Component } from 'react' ;import ReactDOM from 'react-dom' class MyComponet extends Component { constructor (props ) { super (props); this .lists = this .props .nums .map (e => <li key ={e.toString()} > {e}</li > ); } render ( ) { return <ul > {this.lists}</ul > ; } } const numAry = [1 , 2 , 3 ];ReactDOM .render ( <MyComponet nums ={numAry} /> , document .getElementById ('root' ) );
你用不到的 Key 接續前話,這裡的 key 是幫助 React 進行識別用而不是給開發人員使用的。用途為判斷哪些元素被改變(新增修改刪除),有以下特性:
每個元素的 key 都必須在同個陣列內要有唯一性才能讓 React 去追蹤這些元素。只要確保 rander 該指定陣列時能透過 key 判斷出此陣列底下的這些獨立元素。
key 的指定為在 JSX 元素上透過屬性來賦予(類似 props 方式),但是你無法讀取此值,即便 props.key 也做不到。
要從資料哪裡當作 Key 值沒有一定要求,但有一些建議的做法:
使用 id 為 key 從資料庫獲得的資料來源,其欄位 id 具備唯一識別性是很好的方法。
const todoItems = todos.map ((todo ) => <li key ={todo.id} > {todo.text} </li > );
使用 Array’s index 為 key(不建議) 如果資料沒有像 id 這種可當作識別性時,部分人會使用 index 值當作 key。但事實上不太建議這樣做,因為假設資料來源變動,index 重新順序時發生變化造成 React 誤會原本元素與其他元素誤認相同,也會導致判斷過多性能變差。
const todoItems = todos.map ((todo, index ) => <li key ={index} > {todo.text} </li > );
利用第三方 Nano ID 如果沒有 id 又必須要確保每個元素的唯一值,可透過安裝 npm install --save nanoid
來協助我們快速提供雜湊後的短 id 確保唯一性。安裝完畢後,使用示範將前例調整如下:
import { Component } from 'react' ;import ReactDOM from 'react-dom' import { nanoid } from 'nanoid' ; class MyComponet extends Component { constructor (props ) { super (props); this .lists = this .props .nums .map (e => { const myid = nanoid (5 ); console .log (myid); return <li key ={myid} > {e}</li > ; }); } render ( ) { return <ul > {this.lists}</ul > ; } } const numAry = [1 , 2 , 3 ];ReactDOM .render ( <MyComponet nums ={numAry} /> , document .getElementById ('root' ) );
Key 的出現位置 key 的指定處是落在於一陣列內的 JSX 元素,才能當 render 陣列時進行處理 key 的規劃。以下例子為組合 Componet 下的示範說明:
import ReactDOM from 'react-dom' ;import { nanoid } from 'nanoid' ;function ListItem (props ) { return <li > {props.value}</li > ; } function NumberList (props ) { const listItems = props.numbers .map ((number ) => <ListItem key ={nanoid()} value ={number} /> ); return <ul > {listItems}</ul > ; } const numbers = [1 , 2 , 3 ];ReactDOM .render ( <NumberList numbers ={numbers} /> , document .getElementById ('root' ) );
或者簡潔的一次寫完,利用 JSX 的{}
表達式特性。
import ReactDOM from 'react-dom' import { nanoid } from 'nanoid' ;function ListItem (props ) { return <li > {props.value}</li > ; } function NumberList (props ) { return ( <ul > {props.numbers.map((number) => <ListItem key ={nanoid()} value ={number} /> )} </ul > ); } const numbers = [1 , 2 , 3 ];ReactDOM .render ( <NumberList numbers ={numbers} /> , document .getElementById ('root' ) );
表單設計 透過 html 表單元素能跟用戶進行資料處理,搭配 state 與 event 可以即時性雙向綁定作業,讓方便性提高不少。在 React 實施雙向綁定的方式稱呼為 Controlled Component 受控組件,讓用戶輸入完成動態更新 state 值而透過 setstate() 達到。但也不是什麼都能控制,例如 file 只能用戶來進行修改,無法透過 JavaScript 來反向修改(僅唯讀),這一類型又稱呼為 uncontrolled component 不受控組件。
嘗試將以下 input 版型改為 controlled component,並取消 form 的 submit 預設提交動作。按下 submit 就提供 alert 做檢查。
<form > <label > Name: <input type ="text" name ="name" /> </label > <input type ="submit" value ="Submit" /> </form >
首先,由於 input 元素的 value 屬性必需換成 state 值。
由於 input 的 value 值被指定給 state,因此若需要呈現 HTML 的 value 預設值,就透過建構子 constructor 來賦予初始化。既使沒有預設值也應該給予 state 預設當下為空字串。
input 這裡需每次發生 change 事件時,呼叫函式方法來更新 state 值,使得保持最新。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { constructor (props ) { super (props); this .state = { myValue : '' }; this .doChange = this .doChange .bind (this ); this .doSubmit = this .doSubmit .bind (this ); } doChange (event ) { this .setState ({ myValue : event.target .value }, () => console .log (this .state ) ); } doSubmit (event ) { event.preventDefault (); console .log ('state' , this .state ); } render ( ) { return ( <form onSubmit ={this.doSubmit} > <label > Name: <input type ="text" name ="name" value ={this.state.myValue} onChange ={this.doChange} /> </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
input[type=file]
本身是唯獨屬性,React 只歸類為 uncontrolled component 不受控組件無法雙向綁定。
Textarea Textarea 元素眾所皆知他的值為 Content。其 JSX 語法觀念同樣透過屬性 value 來賦予,這在編寫上習慣與 input 一致。將以下 textarea 版型轉為 controlled component 同上要求:
<form > <label > Message: <textarea > Hello there, this is some text in a text area </textarea > </label > <input type ="submit" value ="Submit" /> </form >
與前面 input 差不多,唯獨因為 textarea 在 JSX 內的值表示改為 value 屬性來提供,同時 JSX 語法不需要結尾標籤,可改用/>
表示結尾。
textarea 預設值透過建構子來指定 value 一開始的內容。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { constructor (props ) { super (props); this .state = { myValue : 'Hello there, this is some text in a text area' }; this .doChange = this .doChange .bind (this ); this .doSubmit = this .doSubmit .bind (this ); } doChange (event ) { this .setState ({ myValue : event.target .value }); } doSubmit (event ) { event.preventDefault (); console .log ('state' , this .state ); } render ( ) { return ( <form onSubmit ={this.doSubmit} > <label > Message: <textarea value ={this.state.myValue} onChange ={this.doChange} /> </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
Select Select 的預設選定值為透過select>option[selected]
來獲得,而 JSX 的預設值的方式採用select[value=*]
來檢查與option[value]
作為選定值。
<select > <option value ="grapefruit" > Grapefruit</option > <option value ="lime" > Lime</option > <option selected value ="coconut" > Coconut</option > <option value ="mango" > Mango</option > </select >
假若要預設選定 option,則建構子上榜定此值。
import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { constructor (props ) { super (props); this .state = { myValue : 'coconut' }; this .doChange = this .doChange .bind (this ); this .doSubmit = this .doSubmit .bind (this ); } doChange (event ) { this .setState ({ myValue : event.target .value }); } doSubmit (event ) { event.preventDefault (); console .log ('state' , this .state ); } render ( ) { return ( <form onSubmit ={this.doSubmit} > <label > Likes: <select value ={this.state.myValue} onChange ={this.doChange} > <option value ="grapefruit" > Grapefruit</option > <option value ="lime" > Lime</option > <option value ="coconut" > Coconut</option > <option value ="mango" > Mango</option > </select > </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
checkbox & radio checkbox 與 radio 元素的 value 值一直都會存在,唯獨當賦予 checked 屬性才會被表單所選入提交。因此表單資料的重點為 checked 屬性是否存在而不是 value 屬性。
<form > Is going: <label > Home <input name ="isGoing1" type ="checkbox" value ="home" /> </label > <label > Office <input name ="isGoing2" type ="checkbox" checked value ="office" /> </label > <br /> Gender: <label > Man <input name ="gender" type ="radio" value ="man" /> </label > <label > Woman <input name ="gender" type ="radio" value ="woman" checked /> </label > </form >
是否該元素的 checked 是否存在根據需判斷來源是 target 的 value 或是 checked 狀況。本篇開始因代碼量較長開始習慣一些進階操作減少代碼。
src/index.js import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { state = { home : false , office : true , gender : 'woman' }; doChangeGo1 = event => { this .setState ({ home : event.target .checked }); } doChangeGo2 = event => { this .setState ({ office : event.target .checked }); } doChangeRDO = event => { this .setState ({ gender : event.target .value }); } render ( ) { const { home, office, gender } = this .state ; return ( <div > <form > Is going: <label > Home <input name ="home" type ="checkbox" value ="home" checked ={home} onChange ={this.doChangeGo1} /> </label > <label > Office <input name ="office" type ="checkbox" value ="office" checked ={office} onChange ={this.doChangeGo2} /> </label > <br /> Gender: <label > Man <input name ="gender" type ="radio" value ="man" checked ={gender === 'man' } onChange ={this.doChangeRDO} /> </label > <label > Woman <input name ="gender" type ="radio" value ="woman" checked ={gender === 'woman' } onChange ={this.doChangeRDO} /> </label > </form > <pre > {JSON.stringify(this.state, null, 2)}</pre > </div > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
多欄位設計 前面個別介紹這種表單欄位元素對應一個事件函式在同 controlled component 內的設計,如果 N 個欄位元素也隨著要定義 N 個事件函式使用,這裡示範如何改善使用同個 事件函式做共用(仍需透過判斷來做動作)。以前例子做改變:
src/index.js import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { state = { home : false , office : true , gender : 'woman' }; doChange = event => { const key = event.target .name , val = event.target .type === 'checkbox' ? event.target .checked : event.target .value ; this .setState ({ [key]: val }); } render ( ) { const { home, office, gender } = this .state ; return ( <div > <form > Is going: <label > Home <input name ="home" type ="checkbox" value ="home" checked ={home} onChange ={this.doChange} /> </label > <label > Office <input name ="office" type ="checkbox" value ="office" checked ={office} onChange ={this.doChange} /> </label > <br /> Gender: <label > Man <input name ="gender" type ="radio" value ="man" checked ={gender === 'man' } onChange ={this.doChange} /> </label > <label > Woman <input name ="gender" type ="radio" value ="woman" checked ={gender === 'woman' } onChange ={this.doChange} /> </label > </form > <pre > {JSON.stringify(this.state, null, 2)}</pre > </div > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
然而,隨著使用量大變有規劃深度,你可能會透過 nesting 方式來做 state,問題在於 setState() 他是 marge 原理所以並不適用在嵌套上,所以需要技巧的利用擴展運算子來處理修改。
src/index.js import { Component } from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends Component { state = { isGoing : { home : false , office : true , }, gender : 'woman' }; doChange = event => { const key = event.target .name , val = event.target .type === 'checkbox' ? event.target .checked : event.target .value ; this .setState (state => { if (event.target .type === 'radio' ) return ({ [key]: val }); return ({ isGoing : { ...state.isGoing , [key]: val } }); }); } render ( ) { const { isGoing, gender } = this .state ; return ( <div > <form > Is going: <label > Home <input name ="home" type ="checkbox" value ="home" checked ={isGoing.home} onChange ={this.doChange} /> </label > <label > Office <input name ="office" type ="checkbox" value ="office" checked ={isGoing.office} onChange ={this.doChange} /> </label > <br /> Gender: <label > Man <input name ="gender" type ="radio" value ="man" checked ={gender === 'man' } onChange ={this.doChange} /> </label > <label > Woman <input name ="gender" type ="radio" value ="woman" checked ={gender === 'woman' } onChange ={this.doChange} /> </label > </form > <pre > {JSON.stringify(this.state, null, 2)}</pre > </div > ); } } ReactDOM .render ( <DemoForm /> , document .getElementById ('root' ) );
controlled component 下的固定值 如果 JSX 語法上對某元素指定了 value 值(除了 null 或 undefined),不向 HTML 觀念可被修改,React 會永遠保持此值不變。
import ReactDOM from 'react-dom' ;ReactDOM .render (<input value ="hi" /> , document .getElementById ('root' ));setTimeout (function ( ) { ReactDOM .render (<input value ={null} /> , document .getElementById ('root' )); }, 3000 );
Uncontrolled Component 相較於 Controlled Component 設計是十分繁瑣的,尤其是每個元素都要設定事件函式動作加入 state 規劃。如果從某舊專案轉換或其他函式庫整合會很麻煩。因此可以使用 Uncontrolled Component 非受控組件,也就是捨棄 React 的 state 來處理,使這些表單值改由 JavaScript 本身 DOM 來處理。設計 Uncontrolled Component 最大的重點是沒有 state 而是透過 ref 獲得 DOM 的資料。
import React from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends React.Component { myInput = React .createRef (); doSubmit = event => { event.preventDefault (); alert ('A name was submitted: ' + this .myInput .current .value ); } render ( ) { return ( <form onSubmit ={this.doSubmit} > <label > Name: <input type ="text" ref ={this.myInput} /> </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render (<DemoForm /> , document .getElementById ('root' ));
內容值的位置在 red 之 current 底下,可以透過例如console.log(this.myInput);
來獲得理解物件內容。
非受控組件採用 DOM 原本值的來源,整體代碼較短但效能較差,但能較容易與其他函式庫進行整合。如果可以還是盡量改用受控組件來設計表單。
預設值 非受控組件本身 React 不會協助處理,但你可要求 React 幫忙指定預設值之後就不會管這些值處理。透過 JSX 元素之屬性defaultValue
(Input,textarea,select) 或defaultChecked
(checkbox, radio) 達到。
import React from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends React.Component { myInput = React .createRef (); doSubmit = event => { event.preventDefault (); alert ('A name was submitted: ' + this .myInput .current .value ); console .log (this .myInput ); } render ( ) { return ( <form onSubmit ={this.handleSubmit} > <label > Name: <input defaultValue ="Bob" type ="text" ref ={this.myInput} /> </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render (<DemoForm /> , document .getElementById ('root' ));
稍早提到input[type=file]
永遠都是 uncontrolled component,因為它的值只能被使用者設定,而無法由程式碼來設定。以下示範如何透過 ref 來獲得檔案內容。
import React from 'react' ;import ReactDOM from 'react-dom' ;class DemoForm extends React.Component { myFile = React .createRef (); doSubmit = event => { event.preventDefault (); alert (`Selected file - ${this .myFile.current.files[0 ].name} ` ); } render ( ) { return ( <form onSubmit ={this.doSubmit} > <label > Name: <input type ="file" ref ={this.myFile} /> </label > <input type ="submit" value ="Submit" /> </form > ); } } ReactDOM .render (<DemoForm /> , document .getElementById ('root' ));
最後,React 的表單設計已基礎介紹完畢,若需要找到更好的擴充 DLC。官方推薦了 https://formik.org/的學習,他是一個完整的、包含驗證、可追蹤拜訪欄位並能處理提交表單等功能的解決方案。
提升 state 前面提到在組合式組件應用上,state 本身會有單向資料流的特性。
此例為透過用戶輸入攝氏溫度值來回饋適合之文字。為設計兩個上下組件 Calculator 與 BoilingVerdict,透過 Calculator(parent) 的 state 提供給 BoilingVerdict(child) 的 props 來進行分解。
BoilingVerdict 負責從 Calculator 取得用戶的輸入值來提供適合的回應文字。
Calculator 主要負責 input&event 設計、BoilingVerdict 回饋的文字,並將這些全渲染畫面出來。
import { Component } from "react" ;import ReactDOM from 'react-dom' ;function BoilingVerdict (props ) { if (props.celsius >= 100 ) return <p > 水溫已沸點</p > ; return <p > 水溫未沸點</p > } class Calculator extends Component { state = { temperature : '' } doChange = e => { this .setState ({ temperature : e.target .value }) } render ( ) { const temperature = this .state .temperature ; return ( <fieldset > <legend > 請輸入攝氏溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> <BoilingVerdict celsius ={temperature} /> </fieldset > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
然後,改一下內容規劃成兩個 input,我們需要兩個類似的 input 提供華氏與攝氏的輸入。因此 Calculator 組件內的 input 與 event 相關代碼另外分解出來做成 TemperatureInput 組件
BoilingVerdict 暫時用不到,註解藏起來,render 那裏獨立出來並先註解該元素標籤。
Calculator 兩組 TemperatureInput 回饋的 JSX 元素,並將這些全渲染畫面出來。
TemperatureInput 主要負責 input&event 設計。
import { Component } from "react" ;import ReactDOM from 'react-dom' ;const scaleName = { f : "攝氏" , c : "華氏" }; class TemperatureInput extends Component { state = { temperature : '' } doChange = e => { this .setState ({ temperature : e.target .value }) } render ( ) { const temperature = this .state .temperatur , scale = this .props .scale ; return ( <fieldset > <legend > 請輸入{scaleName[scale]}溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> </fieldset > ); } } class Calculator extends Component { render ( ) { return ( <div > <TemperatureInput scale ="c" /> <TemperatureInput scale ="f" /> {/* <BoilingVerdict celsius ={temperature} /> */} </div > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
現在還差 2 種溫度的轉換計算,。並設計具備檢查(有效輸入)、引用公式、後續處理(小數點處理)的主要轉換之函式。我們編列在 Calculator 組件內稍晚在解釋如何應用這些公式轉換函式
import { Component } from "react" ;import ReactDOM from 'react-dom' ;const scaleName = { f : "攝氏" , c : "華氏" }; class TemperatureInput extends Component { state = { temperature : '' } doChange = e => { this .setState ({ temperature : e.target .value }) } render ( ) { const temperature = this .state .temperatur , scale = this .props .scale ; return ( <fieldset > <legend > 請輸入{scaleName[scale]}溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> </fieldset > ); } } class Calculator extends Component { toCelsius = tempF => (tempF - 32 ) * 5 / 9 ; toFahrenheit = tempC => (tempC * 9 / 5 ) + 32 ; tryConvert (temp, convert ) { const inputVal = parseFloat (temp); if (Number .isNaN (inputVal)) return '' ; const outputVal = convert (inputVal); const rounded = Math .round (outputVal * 1000 ) / 1000 ; return rounded.toString (); } render ( ) { return ( <div > <TemperatureInput scale ="c" /> <TemperatureInput scale ="f" /> {/* <BoilingVerdict celsius ={temperature} /> */} </div > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
現在最大的重頭戲,如何讓兩個 TemperatureInput 組件各自的 local state 能彼此互相分享。這是不可逆流的單向資料流,解決方法便是透過提升 state 將綁定轉移到上層的 Calculator 組件(祖先),由父組件負責管理 state,而子組件負責 event 事件的傳達。我們希望兩個爛位的值來自同一個 state,這樣才能在 state 更動時兩個欄位也是動態來自同出處,使得形成反向資料流。
將子組件的 local state 轉移到父組件,由父組件來負責 local state 透過 props 來提供給子組件。
原本的 state 提升到父組件去了,因此子組件原本的 this.state 寫法改成 this.props
子組件的 setState 之函式先複製一份到父組件去並替換函式名稱,父組件讓可以控制他自己的 local state。
子組件原本的 event 事件是要控至自己的 setState,現在跟畫面有關的 state 已經在父組件內,子組件是不可能控制父組件的 state。但可以透過 props 請父組件將他的 setState 之函式傳過來。讓子組件提供函式參數在父組件的函式上做執行。
跟著完成以下改動後,可試著操作兩邊欄位看看,應該可以共享並已完成提升 state 作業。
import { Component } from "react" ;import ReactDOM from 'react-dom' ;const scaleName = { f : "攝氏" , c : "華氏" }; class TemperatureInput extends Component { doChange = e => { this .props .parentChange (e.target .value ); } render ( ) { const temperature = this .props .temperatur , scale = this .props .scale ; return ( <fieldset > <legend > 請輸入{scaleName[scale]}溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> </fieldset > ); } } class Calculator extends Component { toCelsius = tempF => (tempF - 32 ) * 5 / 9 ; toFahrenheit = tempC => (tempC * 9 / 5 ) + 32 ; tryConvert (temp, convert ) { const inputVal = parseFloat (temp); if (Number .isNaN (inputVal)) return '' ; const outputVal = convert (inputVal); const rounded = Math .round (outputVal * 1000 ) / 1000 ; return rounded.toString (); } state = { temperature : '' } parentChange = temp => { this .setState ({ temperature : temp }) } render ( ) { const temperature = this .state .temperature ; return ( <div > <TemperatureInput scale ="c" temperatur ={temperature} parentChange ={this.parentChange} /> <TemperatureInput scale ="f" temperatur ={temperature} parentChange ={this.parentChange} /> {/* <BoilingVerdict celsius ={temperature} /> */} </div > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
接著要套入公式換算的設計,唯獨要思考的是:
對 F 欄位變化時要觸發 toC 換算;反之對 C 欄位變化觸發 toF 換算。這裡就分了兩個 event 要做。而且還需要一個 state 來做標記該 toC 還是 toF。
不論目前是 toC 還是 toF 的作業之前,這兩個欄位要獲得正確的值輸出。
import { Component } from "react" ;import ReactDOM from 'react-dom' ;const scaleName = { f : "攝氏" , c : "華氏" }; class TemperatureInput extends Component { doChange = e => { this .props .parentChange (e.target .value ); } render ( ) { const temperature = this .props .temperatur , scale = this .props .scale ; return ( <fieldset > <legend > 請輸入{scaleName[scale]}溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> </fieldset > ); } } class Calculator extends Component { toCelsius = tempF => (tempF - 32 ) * 5 / 9 ; toFahrenheit = tempC => (tempC * 9 / 5 ) + 32 ; tryConvert (temp, convert ) { const inputVal = parseFloat (temp); if (Number .isNaN (inputVal)) return '' ; const outputVal = convert (inputVal); const rounded = Math .round (outputVal * 1000 ) / 1000 ; return rounded.toString (); } state = { temperature : '' , flagFC : '' } parentChangetoC = temp => { this .setState ({ temperature : temp, flagFC : 'toC' }); } parentChangetoF = temp => { this .setState ({ temperature : temp, flagFC : 'toF' }); } render ( ) { const temperature = this .state .temperature ; const tempC = (this .state .flagFC === "toC" ) ? this .tryConvert (temperature, this .toCelsius ) : temperature; const tempF = (this .state .flagFC === "toF" ) ? this .tryConvert (temperature, this .toFahrenheit ) : temperature; return ( <div > <TemperatureInput scale ="c" temperatur ={tempC} parentChange ={this.parentChangetoF} /> <TemperatureInput scale ="f" temperatur ={tempF} parentChange ={this.parentChangetoC} /> {/* <BoilingVerdict celsius ={temperature} /> */} </div > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
最後,把沸點的動態文字放回來,現在控制溫度的變數換成攝氏那個變數。
import { Component } from "react" ;import ReactDOM from 'react-dom' ;const scaleName = { f : "攝氏" , c : "華氏" }; function BoilingVerdict (props ) { if (props.celsius >= 100 ) return <p > 水溫已沸點</p > ; return <p > 水溫未沸點</p > } class TemperatureInput extends Component { doChange = e => { this .props .parentChange (e.target .value ); } render ( ) { const temperature = this .props .temperatur , scale = this .props .scale ; return ( <fieldset > <legend > 請輸入{scaleName[scale]}溫度</legend > <input type ="text" value ={temperature} onChange ={this.doChange} /> </fieldset > ); } } class Calculator extends Component { toCelsius = tempF => (tempF - 32 ) * 5 / 9 ; toFahrenheit = tempC => (tempC * 9 / 5 ) + 32 ; tryConvert (temp, convert ) { const inputVal = parseFloat (temp); if (Number .isNaN (inputVal)) return '' ; const outputVal = convert (inputVal); const rounded = Math .round (outputVal * 1000 ) / 1000 ; return rounded.toString (); } state = { temperature : '' , flagFC : '' } parentChangetoC = temp => { this .setState ({ temperature : temp, flagFC : 'toC' }); } parentChangetoF = temp => { this .setState ({ temperature : temp, flagFC : 'toF' }); } render ( ) { const temperature = this .state .temperature ; const tempC = (this .state .flagFC === "toC" ) ? this .tryConvert (temperature, this .toCelsius ) : temperature; const tempF = (this .state .flagFC === "toF" ) ? this .tryConvert (temperature, this .toFahrenheit ) : temperature; return ( <div > <TemperatureInput scale ="c" temperatur ={tempC} parentChange ={this.parentChangetoF} /> <TemperatureInput scale ="f" temperatur ={tempF} parentChange ={this.parentChangetoC} /> <BoilingVerdict celsius ={tempC} /> </div > ); } } ReactDOM .render ( <Calculator /> , document .getElementById ('root' ) );
Component 組合模型應用 使用組件進行組合時,透過 JSX 進行直接組合。前面介紹的用法都是直接使用<元素名稱/>
來應用,並允許添加屬性做成 props 進行傳遞。事實上也可以透過結尾標籤來應用組合。透過此方式你不需要利用 class 繼承其他 class 獲得資源之引用問題。
childeren 內容版型 包覆在元素標籤的內容也是 props 的一種,可以透過 props.children 一併提供給子組件使用。以下例示範:
import { render } from "react-dom" ;function FancyBorder (props ) { return ( <div className ={ 'FancyBorder FancyBorder- ' + props.color }> {props.children} </div > ); } function WelcomeDialog ( ) { return ( <FancyBorder color ="blue" > <h1 className ="Dialog-title" > Welcome </h1 > <p className ="Dialog-message" > Thank you for visiting our spacecraft! </p > </FancyBorder > ); } render (<WelcomeDialog /> , document .getElementById ('root' ));
透過 children 可以設計出一個父組件能根據子組件 A1 跟 A2 不同 Layout 版型需求而提供不同的 children。
多個內容版型 如果內容包覆的有多個因此一個 children 不夠用時。可以改用 props 屬性來指定不同版型組件同時傳遞多個版型。
import { render } from "react-dom" ;function Contacts ( ) { return <div className ="Contacts" /> ; } function Chat ( ) { return <div className ="Chat" /> ; } function SplitPane (props ) { return ( <div className ="SplitPane" > <div className ="SplitPane-left" > {props.left} </div > <div className ="SplitPane-right" > {props.right} </div > </div > ); } function App ( ) { return ( <SplitPane left ={ <Contacts /> } right={<Chat /> } /> ); } render ( <App /> , document .getElementById ('root' ) );
特別情況混和 有些情況下想使用通用版型下的差異組合,前兩者是可很混合使用的。例如下面:
FancyBorder 是一個通用版型,會去跟 Dialog 拿取 children 內容形成畫面。
Dialog 也是一個仲介版型,會去 WelcomeDialog 拿取兩段內容形成畫面。
WelcomeDialog 本身可決定要提供差異內容給 Dialog,使得最終畫面有不同差異內容的版型結果。
import { render } from "react-dom" ;function FancyBorder (props ) { return ( <div className ={ 'FancyBorder FancyBorder- ' + props.color }> {props.children} </div > ); } function Dialog (props ) { return ( <FancyBorder color ="blue" > <h1 className ="Dialog-title" > {props.title} </h1 > <p className ="Dialog-message" > {props.message} </p > </FancyBorder > ); } function WelcomeDialog ( ) { return ( <Dialog title ="Welcome" message ="Thank you for visiting our spacecraft!" /> ); } render ( <WelcomeDialog /> , document .getElementById ('root' ) );
參考文獻