第 3 章:React 元件詳解
元件是 React 應用程式的基石。它們是獨立且可重用的程式碼片段,用於定義 UI 的一部分。透過將 UI 拆分成多個元件,您可以更輕鬆地管理、測試和複用程式碼。
3.1 什麼是元件?
在概念上,元件就像 JavaScript 函式。它們接受任意的輸入 (稱為 "props",即屬性) 並回傳描述螢幕上應該顯示什麼內容的 React 元素。
React 主要有兩種型別的元件:
- 函式元件 (Functional Components)
- 類別元件 (Class Components)
在現代 React 開發中 (特別是自 React 16.8 引入 Hooks 之後),函式元件因其簡潔性和易用性而成為主流。
3.2 函式元件 (Functional Components)
3.2.1 基本語法
函式元件是最簡單的定義元件的方式。它是一個普通的 JavaScript 函式,接收一個 props
物件作為其第一個參數 (或者使用解構語法直接獲取 props 中的特定屬性),並回傳一個 React 元素。
import React from 'react'; // 在某些舊設定中或為了明確性,可能需要匯入
// 一個簡單的函式元件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 使用 ES6箭頭函式定義函式元件 (更常見)
const Greeting = (props) => {
return <p>Greetings, {props.user}!</p>;
};
// 使用解構賦值獲取 props
const UserProfile = ({ name, age }) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
};
// 您可以這樣使用這些元件:
function App() {
return (
<div>
<Welcome name="Alice" />
<Greeting user="Bob" />
<UserProfile name="Charlie" age={30} />
</div>
);
}
export default App;
在 @learning-react-go.mdc
指引手冊和 @memos
專案的範例中,例如 web/src/components/Common/Button.tsx
,您會看到大量使用箭頭函式定義的函式元件:
// 檔案: web/src/components/Common/Button.tsx (根據 mdc 簡化示意)
import { Button as JoyButton } from '@mui/joy';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
// ... 其他 props
}
// 這就是一個函式元件
export const Button = ({ children, onClick }: ButtonProps) => {
return (
<JoyButton onClick={onClick}>
{children}
</JoyButton>
);
};
3.2.2 為什麼推薦函式元件?
- 更簡潔、易讀:相比類別元件,函式元件的語法更少,程式碼量更小,更容易理解。
- 易於測試:函式元件通常是純函式 (給定相同的 props,回傳相同的輸出),這使得它們非常容易進行單元測試。
- Hooks 的支援:React Hooks (如
useState
,useEffect
等) 讓函式元件也能夠擁有狀態 (state) 和生命週期等特性,使其功能與類別元件相當,甚至在某些方面更靈活。 - 效能潛力:雖然差異可能微乎其微,但在未來 React 的優化中,函式元件可能有更好的效能潛力。
- 更好的
this
處理:函式元件中沒有this
關鍵字的問題,避免了類別元件中常見的this
綁定困擾。
由於這些優勢,React 社群和官方文件都推薦在新專案中優先使用函式元件和 Hooks。
3.3 類別元件 (Class Components)
儘管函式元件是主流,但了解類別元件仍然有其價值,因為您可能會在舊專案中遇到它們,或者在極少數特定情況下需要使用它們。
3.3.1 基本語法與 render()
方法
類別元件是使用 ES6 的 class
語法定義的,並且必須繼承自 React.Component
。類別元件至少需要實作一個名為 render()
的方法,該方法回傳一個 React 元素。
import React from 'react';
class WelcomeMessage extends React.Component {
render() {
// Props 是透過 this.props 存取的
return <h1>Hello, {this.props.name} from Class Component!</h1>;
}
}
// 使用
function App() {
return <WelcomeMessage name="Class User" />;
}
export default App;
在類別元件中:
- Props 透過
this.props
存取。 - 狀態 (State) 透過
this.state
存取,並透過this.setState()
方法更新 (我們將在後續章節詳細討論 State)。 - 生命週期方法 (如
componentDidMount
,componentWillUnmount
等) 也是類別元件的特性 (現在大多可以用useEffect
在函式元件中實現)。
3.3.2 何時可能仍需使用類別元件 (如錯誤邊界)
雖然 Hooks 的出現大大減少了對類別元件的需求,但有一個重要的特性目前仍然只能透過類別元件實現:錯誤邊界 (Error Boundaries)。
錯誤邊界是一種特殊的 React 元件,它可以捕獲其子元件樹中發生的 JavaScript 錯誤,記錄這些錯誤,並顯示一個備用的 UI,而不是讓整個應用程式崩潰。
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以顯示備用 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 您也可以將錯誤記錄到錯誤回報服務
console.error("Uncaught error:", error, errorInfo);
this.setState({ errorInfo: errorInfo });
}
render() {
if (this.state.hasError) {
// 您可以渲染任何自訂的備用 UI
return (
<div>
<h2>Something went wrong.</h2>
{this.state.errorInfo && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
// 通常情況下,只需渲染子元件即可
return this.props.children;
}
}
export default ErrorBoundary;
然後您可以這樣使用它:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
如果 <MyWidget />
或其任何子元件拋出錯誤,ErrorBoundary
將會捕獲它並顯示備用 UI。
3.4 元件的命名約定
React 元件的名稱必須以大寫字母開頭。這是因為 React 將以小寫字母開頭的標籤視為原生的 DOM 標籤 (如 <div>
, <span>
等),而將以大寫字母開頭的標籤視為 React 元件。
// 正確: 元件名以大寫字母開頭
function MyComponent() {
return <p>This is my component.</p>;
}
const element1 = <MyComponent />;
// 錯誤: 如果 MyComponent 以小寫 myComponent 開頭,React 會認為它是一個 HTML 標籤
// function myComponent() { ... }
// const element2 = <myComponent />; // 這會導致錯誤或非預期行為
3.5 元件的組合與拆分
元件最重要的特性之一是其可組合性。您可以將簡單的元件組合成更複雜的元件,就像搭建積木一樣。
同時,將複雜的 UI 拆分成更小、更易於管理的元件也是一種非常好的實踐。遵循單一職責原則 (Single Responsibility Principle),讓每個元件只做好一件事。
組合範例:
function UserAvatar(props) {
return <img src={props.user.avatarUrl} alt={props.user.name} style={{width: 50, height: 50, borderRadius: '50%'}} />;
}
function UserInfo(props) {
return (
<div className="UserInfo">
<UserAvatar user={props.user} />
<div className="UserInfo-name">{props.user.name}</div>
</div>
);
}
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} /> {/* 組合 UserInfo 元件 */}
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{/* ...日期... */}</div>
</div>
);
}
// 使用 Comment 元件
const commentData = {
author: { name: 'Alice', avatarUrl: '...' },
text: 'Hope you enjoy learning React!',
date: new Date()
};
function App() {
return <Comment author={commentData.author} text={commentData.text} date={commentData.date} />;
}
在上面的例子中,Comment
元件組合了 UserInfo
元件,而 UserInfo
元件又組合了 UserAvatar
元件。
拆分原則:
- 可複用性:如果 UI 的某一部分在多個地方使用,或者它本身足夠複雜,可以將其提取為一個獨立的元件。
- 單一職責:一個元件應該只關心一件事情。如果一個元件做了太多事情,考慮將其拆分。
- 程式碼行數:如果一個元件的程式碼變得過長 (例如超過 100-200 行,這只是一個大致的參考),也可能是拆分它的好時機。
3.6 渲染元件
當 React 遇到一個使用者定義的元件 (如 <Welcome name="Sara" />
) 時,它會將 JSX 屬性 (以及 children
) 作為一個單一的物件 (即 props
) 傳遞給該元件。元件接著回傳 React 元素,React 會將這些元素渲染到 DOM 中。
// 1. 我們呼叫 ReactDOM.createRoot().render() 並傳入 <Welcome name="Sara" />。
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<Welcome name="Sara" />);
// 2. React 呼叫 Welcome 元件,並將 {name: 'Sara'} 作為 props 傳入。
function Welcome(props) {
// props is {name: 'Sara'}
// 3. Welcome 元件回傳一個 <h1>Hello, Sara</h1> 元素作為結果。
return <h1>Hello, {props.name}</h1>;
}
// 4. React DOM 高效地更新 DOM 以匹配 <h1>Hello, Sara</h1>。
第 3 章「React 元件詳解」已完成。這一章介紹了 React 元件的基礎知識、函式元件與類別元件的區別和用法、命名約定以及元件的組合與拆分。