React 中的 Controlled Component 與 Uncontrolled Component
舊版解釋:以表單狀態管理為核心
在舊版的 React 文件中(特別是 React 16 及更早版本),受控元件和非受控元件的定義主要針對表單元素(如 <input>)的狀態管理方式:
-
受控元件(Controlled Component):表單元素的值由 React 的 state 控制。React 透過 value 屬性和 onChange 事件來同步更新 state,確保資料一致性。
-
非受控元件(Uncontrolled Component):表單元素的值由 DOM 自身管理,React 僅透過 ref 在需要時存取值。
這種解釋適用於表單元素,強調 React 是否「主動控制」表單的狀態。
新版解釋:以 props 控制為核心
在較新的 React 文件(例如 React 17 及之後),受控元件和非受控元件的定義被擴展到更廣泛的元件設計,強調元件是否由外部傳入的 props 控制其行為或狀態:
-
受控元件(Controlled Component):元件的行為或狀態完全由父元件傳入的 props 決定。當 props 改變時,元件的顯示或行為會隨之更新,React 不負責內部狀態管理。
-
非受控元件(Uncontrolled Component):元件擁有自己的內部狀態(通常透過 useState 或 useRef),不受外部 props 的直接控制。
新版解釋適用於任何 React 元件,不限於表單元素。例如,一個顯示文字的元件,如果它的內容由 props 決定,就是受控元件;如果它自己管理內容(例如透過內部 state),則是非受控元件。
整合新舊解釋
為了讓你全面理解這兩個概念,以下是整合新舊版本的定義:
-
表單層面(舊版):
-
受控元件:表單元素的值由 React 的 state 控制,透過 value 和 onChange 實現雙向綁定。
-
非受控元件:表單元素的值由 DOM 管理,React 透過 ref 存取值,無需即時同步。
-
-
元件層面(新版):
-
受控元件:元件的行為或狀態由父元件傳入的 props 控制,父元件是「單一真相來源」。
-
非受控元件:元件擁有內部狀態,獨立於父元件的 props,行為較自主。
-
這種整合讓我們可以靈活應用這兩個概念,既適用於表單處理,也適用於一般元件的設計。
實作教學:表單與一般元件的受控與非受控範例
以下我將提供兩個完整的範例,分別展示表單層面(舊版解釋)和一般元件層面(新版解釋)的受控與非受控元件,包含詳細程式碼和操作步驟。
範例 1:表單層面的受控與非受控元件
這個範例展示一個表單,包含一個受控的輸入框(名字)和一個非受控的輸入框(電子郵件),提交時顯示兩者的值。
javascript
import React, { useState, useRef } from "react";
function FormExample() {
// 受控元件的 state
const [name, setName] = useState("");
// 非受控元件的 ref
const emailRef = useRef(null);
// 處理受控元件的輸入變化
const handleNameChange = (event) => {
setName(event.target.value);
};
// 處理表單提交
const handleSubmit = (event) => {
event.preventDefault();
const email = emailRef.current.value;
alert(`名字:${name}\n電子郵件:${email}`);
};
return (
<div>
<h1>表單層面:受控與非受控元件</h1>
<form onSubmit={handleSubmit}>
<label>
名字(受控):
<input type="text" value={name} onChange={handleNameChange} />
</label>
<br />
<label>
電子郵件(非受控):
<input type="email" ref={emailRef} />
</label>
<br />
<button type="submit">提交</button>
</form>
<p>即時顯示名字:{name}</p>
</div>
);
}
export default FormExample;
操作步驟:
-
建立檔案:在你的 React 專案中建立一個新檔案(例如 FormExample.js),複製上述程式碼。
-
運行程式:啟動 React 應用程式(例如使用 npm start)。
-
測試功能:
-
在「名字」輸入框輸入內容,觀察下方
<p>標籤即時顯示輸入值(受控元件)。 -
在「電子郵件」輸入框輸入內容,點擊「提交」按鈕,檢查 alert 顯示的名字和電子郵件(非受控元件)。
-
-
理解差異:名字輸入框的值由 state 控制,即時更新;電子郵件的值由 DOM 管理,僅在提交時透過 ref 取得。
說明:
-
受控元件(名字):透過 useState 管理 name,每次輸入觸發 onChange 更新 state,確保 React 完全控制輸入框的值。
-
非受控元件(電子郵件):透過 useRef 存取
<input>的 DOM 節點,僅在提交時取得值,React 不追蹤其變化。
範例 2:一般元件的受控與非受控元件
這個範例展示一個簡單的顯示文字的元件,分別以受控和非受控方式實現。父元件傳入一個 text prop,受控元件直接使用 prop,非受控元件則有自己的內部狀態。
import React, { useState } from "react";
// 受控元件:完全由 props 控制
function ControlledText({ text }) {
return (
<div>
<h2>受控元件</h2>
<p>顯示文字:{text}</p>
</div>
);
}
// 非受控元件:擁有內部 state
function UncontrolledText({ initialText }) {
const [text, setText] = useState(initialText);
const handleChange = (event) => {
setText(event.target.value);
};
return (
<div>
<h2>非受控元件</h2>
<input type="text" value={text} onChange={handleChange} />
<p>顯示文字:{text}</p>
</div>
);
}
// 父元件:控制受控與非受控元件
function TextExample() {
const [parentText, setParentText] = useState("預設文字");
const handleParentChange = (event) => {
setParentText(event.target.value);
};
return (
<div>
<h1>一般元件:受控與非受控元件</h1>
<label>
父元件輸入:
<input type="text" value={parentText} onChange={handleParentChange} />
</label>
<hr />
<ControlledText text={parentText} />
<UncontrolledText initialText={parentText} />
</div>
);
}
export default TextExample;
操作步驟:
-
建立檔案:在 React 專案中建立一個新檔案(例如 TextExample.js),複製上述程式碼。
-
運行程式:啟動 React 應用程式。
-
測試功能:
-
在父元件的輸入框輸入文字,觀察受控元件(ControlledText)的文字即時隨著父元件的 parentText 變化。
-
非受控元件(UncontrolledText)的輸入框初始值為 parentText,但之後可以獨立輸入,且不會受父元件影響。
-
-
理解差異:
-
受控元件:ControlledText 完全由父元件的 text prop 控制,無內部狀態。
-
非受控元件:UncontrolledText 使用自己的 useState 管理 text,初始值來自 initialText prop,但後續變化獨立於父元件。
-
說明:
-
受控元件(ControlledText):行為完全由父元件的 text prop 決定,適合需要父元件完全控制的場景。
-
非受控元件(UncontrolledText):擁有內部 state,允許獨立操作,適合需要自主行為的元件。
受控元件 vs. 非受控元件:比較表
| 特性 | 受控元件 | 非受控元件 |
|---|---|---|
| 舊版定義(表單) | 值由 React state 控制 | 值由 DOM 管理,透過 ref 存取 |
| 新版定義(一般元件) | 行為由父元件 props 控制 | 擁有內部 state,獨立於 props |
| 資料同步 | 即時同步(state 或 props 更新) | 需手動存取(ref 或內部 state) |
| 程式碼複雜度 | 較高(需處理 state 或 props) | 較低(簡單 ref 或內部 state) |
| 使用場景 | 複雜表單、父元件控制的 UI | 簡單表單、自主行為的元件 |
| 靈活性 | 高(易於驗證、聯動) | 低(控制力較弱) |
什麼時候使用哪個?
-
使用受控元件:
-
表單層面:需要即時驗證、格式化或與其他 UI 聯動(例如搜尋欄、註冊表單)。
-
一般元件層面:父元件需要完全控制子元件的行為(例如統一管理 UI 狀態)。
-
範例場景:動態表單、父子元件同步顯示的資料。
-
-
使用非受控元件:
-
表單層面:簡單表單,只需在提交時取得資料(例如登入表單)。
-
一般元件層面:子元件需要獨立行為,不受父元件影響(例如獨立的輸入框或設定面板)。
-
範例場景:一次性資料收集、獨立運作的子元件。
-
實作練習:結合新舊概念的綜合表單
以下是一個綜合範例,結合表單層面(舊版)和一般元件層面(新版)的受控與非受控元件。這個表單包含一個受控的名字輸入框和一個非受控的電子郵件輸入框,並透過一個受控的顯示元件展示結果。
import React, { useState, useRef } from "react";
// 受控的顯示元件
function ControlledDisplay({ name, email }) {
return (
<div>
<h2>受控顯示元件</h2>
<p>名字:{name}</p>
<p>電子郵件:{email}</p>
</div>
);
}
// 綜合表單元件
function MixedExample() {
// 受控元件的 state
const [name, setName] = useState("");
const [submittedEmail, setSubmittedEmail] = useState("");
// 非受控元件的 ref
const emailRef = useRef(null);
// 處理受控元件的輸入變化
const handleNameChange = (event) => {
setName(event.target.value);
};
// 處理表單提交
const handleSubmit = (event) => {
event.preventDefault();
const email = emailRef.current.value;
setSubmittedEmail(email); // 更新提交後的電子郵件
};
return (
<div>
<h1>綜合範例:表單與一般元件的受控與非受控</h1>
<form onSubmit={handleSubmit}>
<label>
名字(受控):
<input type="text" value={name} onChange={handleNameChange} />
</label>
<br />
<label>
電子郵件(非受控):
<input type="email" ref={emailRef} />
</label>
<br />
<button type="submit">提交</button>
</form>
<ControlledDisplay name={name} email={submittedEmail} />
</div>
);
}
export default MixedExample;
操作步驟:
-
建立檔案:在 React 專案中建立一個新檔案(例如 MixedExample.js),複製上述程式碼。
-
運行程式:啟動 React 應用程式。
-
測試功能:
-
在「名字」輸入框輸入內容,觀察 ControlledDisplay 元件即時顯示名字(受控表單 + 受控顯示)。
-
在「電子郵件」輸入框輸入內容,點擊「提交」按鈕,檢查 ControlledDisplay 元件顯示提交的電子郵件(非受控表單)。
-
-
理解差異:
-
名字輸入框和顯示元件都是受控的,值由 state 和 props 控制。
-
電子郵件輸入框是非受控的,僅在提交時透過 ref 取得值。
-
總結
-
舊版解釋聚焦於表單元素,強調是否由 React state 控制值(受控)或由 DOM 管理(非受控)。
-
新版解釋擴展到一般元件,強調是否由父元件的 props 控制(受控)或擁有內部狀態(非受控)。
-
選擇原則:
-
受控元件適合需要即時控制、驗證或父子聯動的場景。
-
非受控元件適合簡單表單或需要自主行為的元件。
-
-
實務建議:在現代 React 開發中,受控元件更常見,因為它提供更高的可控性和一致性。但對於簡單場景,非受控元件可以減少程式碼量。