React 元件 (Components) 及 Props
基本上,一個能夠完整描述自身長相和邏輯的東西就可以是一個 React Component(元件)。舉一個簡單的例子,一個 HTML <select> 元素就可以是一個 Component:
因為 select 有自己的長相(一個灰色的長方塊、有文字、有一個選項箭頭),也有自己完整的邏輯(點開可以選擇不同選項、選完闔上選項)。
一個 Component 是 React 中最小的單位,在 React 中任何介面都是由元件所組合而成。在 React 元件模組化的概念下,你建構 web 頁面 UI 時,基本上就很像是在堆積木,而每一塊積木就是所謂的元件。
例如,一個電話號碼輸入表單可以是由一個輸入欄位 <TelInput /> component 和一個送出按鈕 <Button /> component 所組成:
<form>
<TelInput />
<Button />
</form>
其中 <TelInput /> 元件的內容可能會像是:
<label>
請輸入電話號碼:
<input name="tel" />
</label>
而其中 <Button /> 元件的內容可能會像是:
<button>送出</button>
React Component 其中一個重要的精神就是複用性(reusable),如果你想要練習元件化的思考模式,你可以練習看一個網頁,想像哪些區塊/介面在另外一個頁面是可以重複被使用的?(例如按鈕可以重複使用在很多不同頁面),這些區塊/介面你就可以切割出一個個獨立的 Component。
宣告 React Component
在現代 React 開發中,我們主要使用 Function Component(函式元件)來宣告元件。
Function Component(推薦)
最簡單也是最推薦的方式,就是用一個 function 宣告一個 React Component:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
function 接受一個稱作 props 的參數,props 含義是 properties、屬性的意思,props 是一個 JavaScript object,內容是從外部傳進來元件的變數;而這個 function 需要返回 React elements。
你也可以使用 Arrow Function 語法:
const Welcome = (props) => {
return <h1>Hello, {props.name}</h1>
}
// 如果只有一行 return,可以更簡潔
const Welcome = (props) => <h1>Hello, {props.name}</h1>
使用解構賦值取得 Props
實務上,我們經常使用 ES6 解構賦值 來取得 props 中的值,這樣程式碼更簡潔:
// 使用解構賦值
function Welcome({ name, age }) {
return (
<div>
<h1>Hello, {name}</h1>
<p>Age: {age}</p>
</div>
)
}
渲染 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>
}
// 使用 Welcome 元件,傳入 name 屬性
const element = <Welcome name="Mike" />
發生了什麼事:
- React 遇到
Welcometag,知道這是一個 React Component - 將 tag 上的屬性
name="Mike"打包成 props 物件{name: 'Mike'} - 呼叫
Welcome元件並傳入 props Welcome元件返回<h1>Hello, Mike</h1>element- React 更新 DOM 顯示結果
組合 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>
)
}
這個例子會顯示三個打招呼的標題,分別是 "Hello, Sara"、"Hello, Cahal"、"Hello, Edite"。
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>
)
}
上例 FancyBorder 中的 props.children 的內容就是它的子元素:
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
多個插槽(Slots)
如果你需要在元件中有多個「插槽」放置不同的內容,可以用自訂的 props:
function SplitPane({ left, right }) {
return (
<div className="SplitPane">
<div className="SplitPane-left">{left}</div>
<div className="SplitPane-right">{right}</div>
</div>
)
}
function App() {
return <SplitPane left={<Contacts />} right={<Chat />} />
}
模組化 / 元件化
我們再來舉一個例子說明模組化/元件化的概念。
這是一個評論 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({ user }) {
return <img className="Avatar" src={user.avatarUrl} alt={user.name} />
}
因為網站的很多頁面可能都有大頭貼,像是個人頁面上、評論區塊上等。另外我們也將 props 的 author 重新命名成更通用(general)的 user。
嗯...也許 UserInfo 也可以被拆出來,它是一個顯示 "user 大頭貼" + "user 名稱" 的元件:
function UserInfo({ user }) {
return (
<div className="UserInfo">
<Avatar user={user} />
<div className="UserInfo-name">{user.name}</div>
</div>
)
}
現在的 Comment 是不是更模組化,看起來也更好維護了呢?
function Comment({ author, text, date }) {
return (
<div className="Comment">
<UserInfo user={author} />
<div className="Comment-text">{text}</div>
<div className="Comment-date">{formatDate(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
}
正確的做法是回傳新的值,而不是修改原本的:
// 好的做法 - 回傳新的值
function withdraw(account, amount) {
return {
...account,
total: account.total - amount,
}
}
Props 的預設值
你可以使用 ES6 的預設參數來設定 props 的預設值:
function Button({ color = 'blue', size = 'medium', children }) {
return (
<button className={`btn btn-${color} btn-${size}`}>
{children}
</button>
);
}
// 使用時
<Button>Click me</Button> // color='blue', size='medium'
<Button color="red">Click me</Button> // color='red', size='medium'