[前端框架] React - 初階


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>

<!-- 自訂 Component -->
<script src="custom.js"></script>
</body>

接著在設計 component 當下,需要透過 React 來 createElement 創造 HTML 元素,以及對這個元素進行 HTML 編寫。最後透過虛擬 DOM 指定哪個元素跟組件並渲染到哪個實體 DOM。

custom.js
'use strict';
const btn = React.createElement; //純 React 的 HTML 元素,屆時需指定為標籤名、屬性、內容等參數

//組件 LikeButton
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'
);
}
}

//實體 DOM
const domContainer = document.querySelector('#btnLoki');

//虛擬 DOM 的渲染
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>

<!-- JSX 用 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<!-- 告知 Babel 這裡的 JS 有 JSX 語法需要轉譯處理 -->
<script type="text/babel" src="custom.js"></script>
</body>

設計組件時,就不用 React 的 createElement,直接將 JSX 寫在組件內即可。而虛擬 DOM 當下只需要直接寫組件並渲染到實體 DOM 上,因為 HTML 元素透過 JSX 直接寫在組件內。

custom.js
'use strict';

//建立組件 LikeButton
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>
);
}
}

//實體 DOM
const domContainer = document.querySelector('#btnLoki');

//虛擬 DOM 的渲染
ReactDOM.render(<LikeButton />, domContainer);

透過 CLI

所謂的 CLI 是提供一個系統輔助工具,能夠快速建立 React 開發所需要的伺服器環境與轉譯系統。React 提供了Create React App的 Node 工具(可另簡稱為React cli)。能無腦的解決環境應用進行開發,推薦新手學習使用。Create React App 本身僅包含了 webpack 與 babel 的前端建置管道,不提供任何後端服務功能。

  1. 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 名稱。

  2. 檢查專案內會多一個 my-app 的應用目錄,以及在 package.json 內可以看到以提供 start 指令。我們將位置切換至應用 APP 位置並啟用網站服務。
    node.js
    cd my-app
    npm start

此時會獲得一個網站網址為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')
);

//render 為 透過虛擬 DOM 渲染到實體 DOM 上,這裡綁訂一個 h1 標籤給 HTML 元素 #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')); //透過函式將 element 物件與 DOM 位置提交

每次 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); //透過 function 不斷要求提供新的 html 塞到指定 UI

Componet 組件

由於 React 採用組件導向的框架,設計方式就是先建立組件接著利用組件來顯示頁面結果。定義組件的作法基礎方式就是採用純 JavaScript 函式來構成,而 React 的組件是採 ES6 的 class 來定義(透過繼承 React.Componet 獲得原型),不過最近新觀念是使用 Hook,未來另起篇幅介紹。

//function componet,簡單的方式
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

//class componet,使用 ES6 Class 觀念
class Welcome extends React.Component {
render() { //表達此 class 組件要呈現的內容
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 componet version
function WelcomeF() {
return <h1>Hello!!</h1>;
}
const elementF = <WelcomeF/>;
ReactDOM.render(elementF, document.querySelector('#demo1'));

//<h1>Hello!!</h1>

///////////////////////////////////////////////////////////////class componet version
class WelcomeC extends React.Component {
render() { //表達此 class 組件要呈現的內容
return <h1>Hello!!</h1>;
}
}
const elementC = <WelcomeC/>;
ReactDOM.render(elementC, document.querySelector('#demo2'));

//<h1>Hello!!</h1>

偷懶技巧

如果寫膩太長可以這樣省略寫法。前提是用途單純只有React.ComponentReactDOM.render

src/index.js
import {Component} from 'react'; //只拿 Component
import {render} from 'react-dom';//只拿 render

class WelcomeC extends Component {
render() { //表達此 class 組件要呈現的內容
return <h1>Hello!!</h1>;
}
}
const elementC = <WelcomeC/>;
render(elementC, document.querySelector('#demo2')); //這裡是 react-dom 的 render

傳遞資料 props

如果這個組件有提供屬性參數會以 props 物件方式來保留,用途廣泛的傳遞資料於內外存取屬性。例如我們對外部組件設定添加myname=Loki,便能在內部組件利用此 props 來獲得進行應用。舉例使用 JSX 的{}表達式來插入 props 物件之變數:

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

///////////////////////////////////////////////////////////////function componet version
function WelcomeF(props) {
return <h1>Hello, {props.myname}!!</h1>;
}
const elementF = <WelcomeF myname="Loki" />;
ReactDOM.render(elementF, document.querySelector('#demo1'));

//<h1>Hello, Loki!!</h1>

///////////////////////////////////////////////////////////////class componet version
class WelcomeC extends React.Component {
render() { //表達此 class 組件要呈現的內容
return <h1>Hello, {this.props.name}!!{this.</h1>;
}
}
const elementC = <WelcomeC name="Loki" />;
ReactDOM.render(elementC, document.querySelector('#demo2'));

//<h1>Hello, Loki!!</h1>

如果是 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() { //表達此 class 組件要呈現的內容
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')
);

//<h1>Hello, Loki!!</h1>

通常 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() { //表達此 class 組件要呈現的內容
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')
);

/***********************************************************************************
<div id="demo1">
<div class="Comment">
<div class="UserInfo">
<img class="Avatar" src="https://placekitten.com/g/64/64" alt="Hello Kitty">
<div class="UserInfo-name">Hello Kitty</div>
</div>
<div class="Comment-text">I hope you enjoy learning React!</div>
<div class="Comment-date">2021/10/26</div>
</div>
</div>
************************************************************************************/
  1. 首先最小單位的開始處理,整體最內部的img.Avatar下手。抽取出來獨立一個 Avatar 組件,而原本 Comment 組件的該處改成組件標籤。這裡故意使用 user 代表為 Comment 提供給 Avatar 專用的 props 變數獨立名稱不受影響。
    src/index.js
    class Avatar extends React.Component { //頭像組件
    render() {
    console.log('Avatar', this.props); //檢查這裡的 props 內容
    // Avatar
    // user: {name: 'Hello Kitty', avatarUrl: 'https://placekitten.com/g/64/64'}
    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); //檢查 props 內容有三組
    // Comment
    // author: {name: 'Hello Kitty', avatarUrl: 'https://placekitten.com/g/64/64'}
    // date: Tue Oct 26 2021 16:34:44 GMT+0800 (台北標準時間) {}
    // text: "I hope you enjoy learning React!"
    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>
    )
    }
    }
  2. 在 Avatar 的上層為 UserInfo,也進行進行抽離並將原本的 Avatar 組件一同搬移至另一組件內,開始變成組合式組件。
    scr/index.js
    class Avatar extends React.Component { //頭像組件
    render() {
    console.log('Avatar', this.props);
    // Avatar
    // user: {name: 'Hello Kitty', avatarUrl: 'https://placekitten.com/g/64/64'}
    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);
    // UserInfo
    // author: {name: 'Hello Kitty', avatarUrl: 'https://placekitten.com/g/64/64'}

    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);
    // Comment
    // author: {name: 'Hello Kitty', avatarUrl: 'https://placekitten.com/g/64/64'}
    // date: Tue Oct 26 2021 16:34:44 GMT+0800 (台北標準時間) {}
    // text: "I hope you enjoy learning React!"

    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>
    );
    }
    }
  3. 完成後整體代碼如下,抽離成組合式組件在大型應用上常見到,但不是什麼都要抽離成組件。經驗上若有些 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 有以下步驟:

  1. 建立一個相同名稱並且繼承 React.Component 的 ES6 class。
  2. 加入一個 render() 的空方法。
  3. 將 function 的內容搬到 render() 方法。
  4. 將 render() 內的 props 替換成 this.props。
  5. 刪除剩下空的 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); //透過 function 不斷要求提供新的 html 塞到指定 UI

先將適合做成的組件建立起來,而時間物件從外面產生透過 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); //透過 function 不斷要求提供新的 html 塞到指定 UI

接著將 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 方式改由生命週期負責更新。

  1. 首先將 JSX 內的 props 改成 state。
  2. 組件 class 宣告建構子 constructor 初始化 this.state 為 Date 物件。
  3. 因為用到 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 { //下層,透過 props 獲得上層的 state
render() {
return <h2>It is {this.props.date.toLocaleTimeString()}.</h2>;
}
}

class Clock extends React.Component { //上層,將 state 值以屬性提供給下層組件
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() { //若 Class 的 tick() 被執行,會改變 state 值
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 會遇到的基本問題。

  1. setState() 指定進去的通常為一個物件資料。不可以直接寫入,例如this.state.comment = 'Hello';其 React 並不會更新。正確寫法為this.setState({comment: 'Hello'})
  2. 由於 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
    }));
  3. setState() 的原理是將所提供的物件進行 Marge 合併。因此可以寫入時提供物件格式給予合併使用。例如
    constructor(props) {
    super(props);
    this.state = {
    posts: [],
    comments: []
    };
    }
    componentDidMount() {
    fetchPosts().then(response => {
    this.setState({ //提供 {post:value}
    posts: response.posts
    });
    });

    fetchComments().then(response => {
    this.setState({ //提供 {comments:value}
    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); //5 秒後移除 DOM
}

接著可以指定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() { //若 Class 的 tick() 被執行,會改變 state 值
this.setState({ date: new Date() });
}

componentDidMount() {
this.timeID = setInterval(() => this.tick(), 1000);

setTimeout(() => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}, 5000); //5 秒後移除 DOM
}

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

    /*
    對應純 HTML 的寫法為
    <button onclick="demoFn()">HTML Event</button>
    */

    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';

    /*
    對應純 HTML 的寫法為
    <form onsubmit="console.log('Submit!!'); return false">
    <button type="submit">Submit</button>
    </form>
    */

    class MySubmit extends React.Component {
    demoFn(e) { //e is SyntheticBaseEvent
    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() { //Method,操作 this. 內的 setState
this.setState({ date: new Date() });
}

componentDidMount() {
this.timeID = setInterval(() => this.tick(), 1000); //執行 Class Method
//...
}
//...
}

然而,假設改由事件來處理某個 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() { //將透過按鈕操作此 Method 進行停止計時
// console.log(this); //undefined

clearInterval(this.timeID);
//發生錯誤,找不到 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` 能在 callback 中被使用,這裡的綁定是必要的:
this.stopTime= this.stopTime.bind(this);
}

tick() {
this.setState({ date: new Date() });
}

componentDidMount() {
this.timeID = setInterval(() => this.tick(), 1000);
}

stopTime() { //將透過按鈕操作此 Method 進行停止計時
// console.log(this); //undefined

clearInterval(this.timeID);
//發生錯誤,找不到 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);
}

//將函式換成以下語法結構,使得 events 執行當下是能吃到 this 來自 Class 本身
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() {
//調整 event handle 透過箭頭匿名函式,是執行 stopTime()
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` 能在 callback 中被使用,這裡的綁定是必要的:
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
//prevState 是指前狀態 State 之舊值
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 判斷:

  1. 設計中組件(邏輯用)來判斷選擇哪個小組件 (UI 用),其後大總成輸出時,只需使用該組件即可。
  2. 先使用變數指定判斷為哪個小組件 (UI 用),其後大總成輸出時組件,只需使用該變數即可。
import { Component } from 'react';
import ReactDOM from 'react-dom';

/******** 透過 Greeting 組件來判斷選擇哪個組件標籤*******/
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 />; //如果前行 return 成立就不會執行到此行,因此不需要 else
}
/*********使用變數判斷選擇儲存哪個組件標籤************* */

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() { //調整 state 為已登入 true
this.setState({ isLoggedIn: true });
}

handleLogoutClick() { //調整 state 為未登入 false
this.setState({ isLoggedIn: false });
}

render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) //ture => 顯示登出按鈕
button = <LogoutBtn onClick={this.handleLogoutClick} />;
else //false => 顯示登入按鈕
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>
);
}

//如果獲得{false},h2 整個元素將不會輸出

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() {
// Method 1
this.setState(
state => (
{ showWarning: !state.showWarning }
)
);

//Method 2
// this.setState({ showWarning: !this.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);
//doubled (5) [2, 4, 6, 8, 10]

利用此特性,將陣列透過 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>
); //key 要賦予什麼隨你喜歡,只要提供識別性即可,這裡就直接將內容字串做為 key 值
}
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) =>
// 請在項目沒有固定的 ID 時才這樣做
<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'; // 引用 Nano ID

class MyComponet extends Component {
constructor(props) {
super(props);
this.lists = this.props.nums.map(e => {
const myid = nanoid(5); //快速產生長度 5 的隨機 id
console.log(myid); //測試用, key 真面目如下
// FtnF2
// sQ4qB
// nXzS3
return <li key={myid}>{e}</li>; //回傳 map 陣列
});

}
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) {
// 這是資料陣列,因此 key 寫在此
const listItems = props.numbers.map((number) =>
<ListItem key={nanoid()} value={number} />
);
return <ul>{listItems}</ul>;
}

const numbers = [1, 2, 3];
ReactDOM.render(
// 渲染時獲得陣列,會根據陣列內的 JSX 元素與 key 值進行處理
<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(
// 渲染時獲得陣列,會根據陣列內的 JSX 元素與 key 值進行處理
<NumberList numbers={numbers} />,
document.getElementById('root')
);

表單設計

透過 html 表單元素能跟用戶進行資料處理,搭配 state 與 event 可以即時性雙向綁定作業,讓方便性提高不少。在 React 實施雙向綁定的方式稱呼為 Controlled Component 受控組件,讓用戶輸入完成動態更新 state 值而透過 setstate() 達到。但也不是什麼都能控制,例如 file 只能用戶來進行修改,無法透過 JavaScript 來反向修改(僅唯讀),這一類型又稱呼為 uncontrolled component 不受控組件。

input

嘗試將以下 input 版型改為 controlled component,並取消 form 的 submit 預設提交動作。按下 submit 就提供 alert 做檢查。

<form>
<label>
Name: <input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
  1. 首先,由於 input 元素的 value 屬性必需換成 state 值。
  2. 由於 input 的 value 值被指定給 state,因此若需要呈現 HTML 的 value 預設值,就透過建構子 constructor 來賦予初始化。既使沒有預設值也應該給予 state 預設當下為空字串。
  3. 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) {
//透過 event.target.value 來找到用戶輸入的值
this.setState({
myValue: event.target.value
}, () =>
console.log(this.state)
);
//由於 setState 作業為非同步,因此需要立即查看可添加第二參數做函式作業進行顯示。
}

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>
  1. 與前面 input 差不多,唯獨因為 textarea 在 JSX 內的值表示改為 value 屬性來提供,同時 JSX 語法不需要結尾標籤,可改用/>表示結尾。
  2. 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>
  1. 假若要預設選定 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 {
//透過 class properties proposal 省下 constructor 寫法
state = {
home: false,
office: true,
gender: 'woman'
};

//隨代碼量越來越高,這裡改用 Public class fields 方式來定義函式來省去 bind
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 //ES6 提供 computed property name 動態計算屬性名
});
}

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'));

//若 null or undefined 則用戶可輸入
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);
// console.log(this.myInput);
}

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:file

稍早提到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}`);
// console.log(this.myFile);
}

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: "華氏"
};

// function BoilingVerdict(props) {
// if (props.celsius >= 100) return <p>水溫已沸點</p>;
// return <p>水溫未沸點</p>
// }

// input 元素做成組件
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>

);
}
}

//整個畫面輸出,透過 props 使兩個 TemperatureInput 組件有不同應用
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: "華氏"
};

// function BoilingVerdict(props) {
// if (props.celsius >= 100) return <p>水溫已沸點</p>;
// return <p>水溫未沸點</p>
// }

// input 元素做成組件
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>

);
}
}

//整個畫面輸出,透過 props 使兩個 TemperatureInput 組件有不同應用
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); //將輸入值傳遞給 convert(可能是 toCelsius 或 toFahrenheit)
const rounded = Math.round(outputVal * 1000) / 1000; //四捨五入至小數點第三位,舉例 9.123456 => 9.123
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 更動時兩個欄位也是動態來自同出處,使得形成反向資料流。

  1. 將子組件的 local state 轉移到父組件,由父組件來負責 local state 透過 props 來提供給子組件。
  2. 原本的 state 提升到父組件去了,因此子組件原本的 this.state 寫法改成 this.props
  3. 子組件的 setState 之函式先複製一份到父組件去並替換函式名稱,父組件讓可以控制他自己的 local state。
  4. 子組件原本的 event 事件是要控至自己的 setState,現在跟畫面有關的 state 已經在父組件內,子組件是不可能控制父組件的 state。但可以透過 props 請父組件將他的 setState 之函式傳過來。讓子組件提供函式參數在父組件的函式上做執行。

跟著完成以下改動後,可試著操作兩邊欄位看看,應該可以共享並已完成提升 state 作業。

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); //state 在阿爸身上,只能請阿爸負責函式,自己負責參數
}

render() {
const
temperature = this.props.temperatur, //state 改成 props 做提升 state 到阿爸去
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')
);

接著要套入公式換算的設計,唯獨要思考的是:

  1. 對 F 欄位變化時要觸發 toC 換算;反之對 C 欄位變化觸發 toF 換算。這裡就分了兩個 event 要做。而且還需要一個 state 來做標記該 toC 還是 toF。
  2. 不論目前是 toC 還是 toF 的作業之前,這兩個欄位要獲得正確的值輸出。
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); //state 在阿爸身上,只能請阿爸負責函式,自己負責參數
}

render() {
const
temperature = this.props.temperatur, //state 改成 props 做提升 state 到阿爸去
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 => { //子組件提供 temp,寫回到父組件的 state 上
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); //state 在阿爸身上,只能請阿爸負責函式,自己負責參數
}

render() {
const
temperature = this.props.temperatur, //state 改成 props 做提升 state 到阿爸去
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 => { //子組件提供 temp,寫回到父組件的 state 上
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'));

/* 結果如下
<div id="root">
<div class="FancyBorder FancyBorder-blue">
<h1 class="Dialog-title">Welcome</h1>
<p class="Dialog-message">Thank you for visiting our spacecraft!</p>
</div>
</div>
*/

透過 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')
);

/*
<div id="root">
<div class="SplitPane">
<div class="SplitPane-left">
<div class="Contacts"></div>
</div>
<div class="SplitPane-right">
<div class="Chat"></div>
</div>
</div>
</div>
*/

特別情況混和

有些情況下想使用通用版型下的差異組合,前兩者是可很混合使用的。例如下面:

  • 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')
);

/*
<div id="root">
<div class="FancyBorder FancyBorder-blue">
<h1 class="Dialog-title">Welcome</h1>
<p class="Dialog-message">Thank you for visiting our spacecraft!</p>
</div>
</div>
*/

參考文獻