Skip to content
Home
Go back

Controlled và Uncontrolled Components trong React

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ầuControlledUncontrolled
Validate/format khi đang gõ (ví dụ tự thêm dấu - vào số điện thoại)Phù hợpKhô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ị inputPhù hợpPhả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ợpKhông phù hợp
Form đơn giản, chỉ cần đọc giá trị lúc submitDùng được nhưng dư thừaPhù 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 inputPhù 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 ưuPhù 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:

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


Share this post:

Previous Post
Hooks nâng cao: useReducer, useRef, useLayoutEffect
Next Post
Validate Form với React Hook Form và Zod