Mục lục
Xem mục lục
Controlled Component là gì
Controlled Component là component mà giá trị của nó do React state quản lý hoàn toàn. Input không tự giữ giá trị của chính nó, mọi thay đổi phải đi qua onChange, cập nhật state, rồi React render lại input với giá trị mới từ state đó.
function ControlledInput() {
const [value, setValue] = useState("");
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
value={value} buộc input luôn hiển thị đúng giá trị đang có trong state. Nếu bỏ onChange, input trở thành read-only, vì không có cách nào để state được cập nhật khi người dùng gõ.
Uncontrolled Component là gì
Uncontrolled Component để DOM tự giữ giá trị của chính nó, giống cách input HTML hoạt động mặc định. React không can thiệp vào từng lần gõ phím, chỉ đọc giá trị khi cần, thường qua ref.
function UncontrolledInput() {
const inputRef = useRef(null);
function handleSubmit() {
console.log(inputRef.current.value);
}
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={handleSubmit}>Gửi</button>
</>
);
}
Không có state nào theo dõi giá trị input theo thời gian thực. Component chỉ “hỏi” DOM giá trị hiện tại đúng lúc cần (khi submit), phần còn lại DOM tự quản lý.
Ví dụ đối chiếu trực tiếp
Cùng là một input nhận tên người dùng, viết theo hai cách:
// Controlled
function ControlledName() {
const [name, setName] = useState("");
return <input value={name} onChange={e => setName(e.target.value)} />;
}
// Uncontrolled
function UncontrolledName() {
const ref = useRef(null);
return <input ref={ref} defaultValue="" />;
}
Controlled: mỗi ký tự gõ vào đều chạy qua setName, gây một lần re-render, và name luôn phản ánh đúng giá trị hiện tại trong suốt vòng đời component. Uncontrolled: gõ phím không chạy qua React, không gây re-render nào, giá trị chỉ được đọc ra khi component chủ động gọi ref.current.value.
So sánh khi nào dùng cách nào
| Nhu cầu | Controlled | Uncontrolled |
|---|---|---|
Validate/format khi đang gõ (ví dụ tự thêm dấu - vào số điện thoại) | Phù hợp | Không phù hợp, không có state để can thiệp theo thời gian thực |
| Disable nút submit dựa trên điều kiện của giá trị input | Phù hợp | Phải tự thêm event listener riêng để theo dõi |
| Đồng bộ giá trị input với state khác (ví dụ ô tìm kiếm lọc danh sách ngay khi gõ) | Phù hợp | Không phù hợp |
| Form đơn giản, chỉ cần đọc giá trị lúc submit | Dùng được nhưng dư thừa | Phù hợp, ít code hơn |
| Tích hợp thư viện ngoài thao tác trực tiếp DOM (ví dụ thư viện mask input không phải component React) | Dễ xung đột vì cả hai cùng ghi vào input | Phù hợp, để thư viện tự quản lý DOM |
| Form rất nhiều field, cần tránh re-render toàn form mỗi lần gõ | Có thể chậm nếu không tối ưu | Phù hợp, không re-render theo từng ký tự |
defaultValue vs value
Hai kiểu dùng hai prop khác nhau để đặt giá trị khởi tạo:
- Controlled dùng
value: React kiểm soát giá trị hiển thị ở mọi thời điểm,valuephải đi kèmonChangeđể input còn nhận input từ người dùng. - Uncontrolled dùng
defaultValue: chỉ đặt giá trị ban đầu lúc mount, sau đó DOM tự quản lý, React không đọc lạidefaultValueở các lần render tiếp theo.
Trộn lẫn cả hai trên cùng một input là cạm bẫy phổ biến:
// Vừa dùng value vừa dùng defaultValue: React cảnh báo, hành vi không rõ ràng
<input value={value} defaultValue="initial" />
React ưu tiên value khi cả hai cùng xuất hiện, nhưng sẽ in cảnh báo ra console vì đây gần như luôn là lỗi vô tình, không phải chủ đích.
Cạm bẫy: chuyển từ uncontrolled sang controlled giữa chừng
Một input bắt đầu với value là undefined (uncontrolled), sau đó nhận value là một chuỗi thật (controlled) ở lần render sau, sẽ khiến React in cảnh báo quen thuộc: “A component is changing an uncontrolled input to be controlled”.
function BuggyInput({ initialValue }) {
const [value, setValue] = useState(initialValue);
// Nếu initialValue là undefined lúc mount, input bắt đầu uncontrolled.
// Khi initialValue đổi thành chuỗi thật ở lần render sau, input
// đột ngột chuyển sang controlled, React cảnh báo ngay lập tức.
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
Cách tránh: luôn đảm bảo value có giá trị xác định ngay từ lần render đầu tiên, dùng chuỗi rỗng làm fallback thay vì để undefined/null lọt vào:
const [value, setValue] = useState(initialValue ?? "");
Hybrid pattern
Khi tự xây một component input dùng lại nhiều nơi (ví dụ Select, Checkbox tùy biến), thường cần hỗ trợ cả hai kiểu: cho phép người dùng component tự quản lý state (controlled) nếu cần, nhưng vẫn hoạt động tốt mà không cần truyền gì thêm (uncontrolled) trong trường hợp đơn giản.
Đây chính là pattern controlled/uncontrolled hybrid đã dùng để xử lý Tabs ở bài Compound Components: component tự giữ state nội bộ làm mặc định, nhưng chấp nhận value/onChange tùy chọn để nhường quyền kiểm soát ra ngoài khi cần.
function CustomCheckbox({ checked, defaultChecked, onChange }) {
const [internalChecked, setInternalChecked] = useState(
defaultChecked ?? false
);
const isControlled = checked !== undefined;
const isChecked = isControlled ? checked : internalChecked;
function handleChange(e) {
if (!isControlled) {
setInternalChecked(e.target.checked);
}
onChange?.(e.target.checked);
}
return <input type="checkbox" checked={isChecked} onChange={handleChange} />;
}
Không truyền checked, component tự quản lý state như uncontrolled thông thường. Truyền checked, component cha nắm toàn quyền, CustomCheckbox chỉ còn nhiệm vụ báo thay đổi qua onChange.
Hiệu năng: uncontrolled tránh re-render mỗi lần gõ phím
Với input controlled, mỗi ký tự gõ vào đều gọi setState, gây một lần re-render. Với form nhỏ, chi phí này không đáng kể. Nhưng với form có hàng chục field cùng nằm trong một component, mỗi lần gõ ở bất kỳ field nào cũng có thể kéo theo re-render toàn bộ form nếu không tách state hoặc dùng React.memo hợp lý, gây giật lag khi gõ nhanh trên máy yếu.
Đây là lý do các thư viện quản lý form phổ biến như React Hook Form mặc định dùng cách tiếp cận gần với uncontrolled: đăng ký (register) input bằng ref thay vì value/onChange, đọc giá trị qua DOM khi submit hoặc khi cần validate, tránh re-render theo từng ký tự gõ. Đổi lại, muốn hiển thị giá trị input theo thời gian thực ở nơi khác trong UI (ví dụ đếm số ký tự còn lại) cần cơ chế riêng (watch), không tự nhiên như đọc thẳng từ state như cách controlled truyền thống.
Tổng kết
- Controlled: giá trị do React state quản lý qua
value/onChange, phù hợp khi cần validate, format, hoặc đồng bộ theo thời gian thực. - Uncontrolled: DOM tự giữ giá trị, React chỉ đọc qua
refkhi cần, phù hợp cho form đơn giản hoặc khi cần tránh re-render theo từng ký tự. valueđi với controlled,defaultValueđi với uncontrolled, không nên trộn lẫn cả hai trên cùng một input.- Input chuyển từ
valuelàundefinedsang có giá trị thật giữa chừng gây cảnh báo “uncontrolled to controlled”, tránh bằng cách luôn có giá trị fallback xác định ngay từ đầu. - Component tái sử dụng có thể hỗ trợ cả hai kiểu bằng hybrid pattern (state nội bộ mặc định,
value/onChangetùy chọn để controlled). - Form nhiều field nên cân nhắc uncontrolled hoặc thư viện như React Hook Form để tránh re-render toàn form mỗi lần gõ phím.