React 元件 (Components) | Props

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

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

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

一個 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。

記得嗎?React 使用 JSX 來寫 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')
);
  1. 我們 call ReactDOM.render()<Welcome name="Mike" /> element 顯示到畫面上
  2. React 遇到 Welcome tag 它會知道這是一個 React Component,接著會 call Welcome component 並傳進 props {name: 'Mike'}
  3. 接著 Welcome 元件執行後會 return <h1>Hello, Mike</h1> elements
  4. 最後 React DOM 會自動更新 HTML DOM 顯示出 <h1>Hello, Mike</h1>

點我看這個例子的結果

React Components 名稱開頭都是大寫 (capital letter),小寫開頭的則是保留給 HTML 元素 (i.e. div, p, ul),所以有注意到 React Components 及其 Elements/tags 的名稱都是大寫開頭的嗎!

搞清楚 React Components, React Component instances 和 React Elements 的差別

我再來解釋一下 React Components, React Component instances 和 React Elements 這三個不同名詞的意思和差異。

拿上面那個例子:

  1. 其中我們用 React Component 語法宣告的元件 Welcome,我們稱作是 React Component。
  2. 再來,我們用 JSX 描述的 React DOM elements 結構,我們稱 element 是 React Elements。
  3. 最後,我們用 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')
);

點我看這個例子的結果

在 React 16 之前,每個 Component 都要有一個 container element,像上面例子中,最外層的 <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>
  );
}

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

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