React 元件 (Components) 及 Props

基本上,一個能夠完整描述自身長相和邏輯的東西就可以是一個 React Component(元件)。舉一個簡單的例子,一個 HTML <select> 元素就可以是一個 Component:

因為 select 有自己的長相(一個灰色的長方塊、有文字、有一個選項箭頭),也有自己完整的邏輯(點開可以選擇不同選項、選完闔上選項)。

Component 的大小和範圍都是由你自己的決定,你覺得適合就好。

一個 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>
記得嗎?React 使用 JSX 來寫 React elements。

使用解構賦值取得 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" />

發生了什麼事:

  1. React 遇到 Welcome tag,知道這是一個 React Component
  2. 將 tag 上的屬性 name="Mike" 打包成 props 物件 {name: 'Mike'}
  3. 呼叫 Welcome 元件並傳入 props
  4. Welcome 元件返回 <h1>Hello, Mike</h1> element
  5. React 更新 DOM 顯示結果
React Components 名稱開頭都是大寫(capital letter),小寫開頭的則是保留給 HTML 元素(如 div, p, ul),所以 React Components 及其 Elements/tags 的名稱都是大寫開頭的!

組合 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>
  )
}

哪些東西適合拆成獨立的元件,這邊提供給大家兩個思考方向:

  1. 容易被重複使用的,像是 Button、Avatar、List
  2. 太複雜、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'