React 元件 (Components) | Props
基本上,一個能夠完整描述自身長相和邏輯的東西就可以是一個 React Component (元件)。舉一個簡單的例子,一個 HTML <select>
元素就可以是一個 Component:
因為 select 有自己的長相 (一個灰色的長方塊、有文字、有一個選項箭頭),也有自己完整的邏輯 (點開可以選擇不同選項、選完闔上選項)。
一個 Component 是 React 中最小的單位,在 React 中任何介面都是由元件所組合而成。在 React 元件模組化的概念下,你建構 web 頁面 UI 時,基本上就很像是在堆積木,而每一塊積木就是所謂的元件。
例如,一個電話號碼輸入表單可以是由一個輸入欄位 <Input />
compoent 和一個送出按鈕 <Button />
compoent 所組成:
<form>
<TelInput />
<Button />
</form>
其中 <TelInput />
元件的內容可能會像是:
<label>
請輸入電話號碼:
<input name="tel" />
</label>
而其中 <Button />
元件的內容可能會像是:
<button>送出</button>
React Component 其中一個重要的精神就是複用性 (reusable),如果你想要練習元件化的思考模式,你可以練習看一個網頁,想像哪些區塊/介面在另外一個頁面是可以重複被使用的?(例如按鈕可以重複使用在很多不同頁面),這些區塊/介面你就可以切割出一個個獨立的 Component。
React Component 的宣告語法
有兩種方法可以讓你宣告 React Component,一種是 Function 的宣告方式,另一種則是 Class 的宣告方式。
Functional Component
最簡單的就是用一個 function 宣告一個 React Component。
例子:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function 接受一個稱作 props
的參數,props 含義是 properties、屬性的意思,props 是一個 JavaScript object 內容是從外部傳進來元件的變數;而這個 function 需要返回 React elements。
Class Component
你也可以用 ES6 class 的語法來寫 React 元件。
import React from 'react'
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
首先 class 需要繼承 React.Component
然後需要實作 render()
這個方法來返回 React elements,而在 class methods 中可以用 this.props
存取 props 物件。
用 class 的宣告方式會比用 function 有更多的功能,後面我們會再繼續聊得更仔細。
渲染 React Component (Rendering a Component)
我們用 JSX 來描述我們頁面上想要呈現的 React DOM elements 結構,在 JSX 中可以有一般的 HTML tags,也可以用我們自己宣告的 React 元件。
當 React 遇到 component tags 時,它會將我們在 tag 上指定的所有屬性,統一放在一個 JavaScript object 中當作參數丟進 React component,這個物件稱作 props
。
例如我們想在頁面上顯示 "Hello, Mike":
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Mike" />;
ReactDOM.render(
element,
document.getElementById('root')
);
- 我們 call ReactDOM.render() 將
<Welcome name="Mike" />
element 顯示到畫面上 - React 遇到
Welcome
tag 它會知道這是一個 React Component,接著會 callWelcome
component 並傳進 props{name: 'Mike'}
- 接著
Welcome
元件執行後會 return<h1>Hello, Mike</h1>
elements - 最後 React DOM 會自動更新 HTML DOM 顯示出
<h1>Hello, Mike</h1>
搞清楚 React Components, React Component instances 和 React Elements 的差別
我再來解釋一下 React Components, React Component instances 和 React Elements 這三個不同名詞的意思和差異。
拿上面那個例子:
- 其中我們用 React Component 語法宣告的元件
Welcome
,我們稱作是 React Component。 - 再來,我們用 JSX 描述的 React DOM elements 結構,我們稱
element
是 React Elements。 - 最後,我們用 ReactDOM.render() 將 React Elements 渲染到頁面上時,React 會在背後建立 React Component instances。
組合 React Components (Composing Components)
React 使用組合 (composition) 的概念,而不是繼承 (inheritance) 的概念,來複用 (reuse) 元件,React 元件間可以透過互相組合、重複利用,就像堆積木一樣。
例如我們可以建立一個 App
元件,而在元件裡面使用 Welcome
元件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<div>...</div>
。但從 React 16 開始,提供了 Fragment 的功能,讓我們可以不用再多包一層 container。props.children
有些元件組合的情況下,元件無法事先知道它下面會有哪些子元件,像是容器 (container) 類型的 Dialog 元件。
props
object 的屬性和 component 的屬性是一一對應的,但有個例外就是 props.children
這個內建屬性,它表示該元件下的所有子元件。
用法像是:
// 這個元件可以幫區塊加個邊框
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>
);
}
ReactDOM.render(
<WelcomeDialog />,
document.getElementById('root')
);
上例 FancyBorder 中的 props.children 的內容就是它的子元件樹:
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
模組化 / 元件化
我們再來舉一個例子說明模組化/元件化的概念。
這是一個評論 Comment
元件:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
基本上這個 Component 是很難被重複利用的,因為一般一個網站的評論只會有一種。
為了增加元件可以被重複使用的機會,我們可以把可能在很多地方都用得到的部分拆分出來!
像是我們可以將 Avatar
拉出來變成一個獨立的元件:
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
因為網站的很多頁面可能都有大頭貼,像是個人頁面上、評論區塊上等。另外我們也將 props 的 author
重新命名成更通用 (general) 的 user
。
現在我們的 Comment
元件變成了:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
嗯...也許 UserInfo
也可以被拆出來,它是一個顯示 "user 大頭貼" + "user 名稱" 的元件,另外的好處是也讓 Comment
元件更簡單好維護些:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
現在的 Comment
是不是更模組化,看起來也更好維護了呢?
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
哪些東西適合拆成獨立的元件,這邊提供給大家兩個思考方向:
- 容易被重複使用的,像是 Button, Avatar, List
- 太複雜、code 很長很難讀的大元件,可以把一個大元件拆成幾個小元件組成
Props 是唯讀的 (Read-Only)
React 的原則,你不能更改傳進 Component 的 props
值,也因為資料是固定的,不會在中途被亂篡改,所以你可以確定一個元件在某個資料 (props) 狀態下的畫面一定會是長什麼樣子 (predictable)。
在這原則下,React Component 運作起來就像是一個 Pure Function,讓你的 UI 不管是在資料處理或畫面顯示都可以更穩定、更不容易意外出錯。
例如下面就不是一個 Pure Function,因為它改動了傳進來的值:
function withdraw(account, amount) {
account.total -= amount;
}