Sửa lỗi 'A component is changing an uncontrolled input to be controlled' trong React

beginner⚛️ React2026-05-03| React 16+, React 17, React 18 — mọi trình duyệt, mọi hệ điều hành. Xuất hiện ở chế độ development khi state của form khởi tạo là undefined hoặc null.

Error Message

Warning: A component is changing an uncontrolled input of type 'text' to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa).
#react#forms#controlled-components#state#input

Lỗi gặp phải

Bạn mở console trình duyệt và thấy ngay:

Warning: A component is changing an uncontrolled input of type 'text' to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.

Form vẫn hoạt động — tạm thời thôi. Nhưng cảnh báo này cho thấy input của bạn đang thay đổi "danh tính" giữa chừng trong quá trình render. Sự thay đổi đó gây ra lỗi thực sự: giá trị cũ bị giữ lại, validation bị hỏng, con trỏ nhảy về cuối khi bạn đang gõ. Đừng bỏ qua nó.

Nguyên nhân

React phân biệt rõ ràng hai loại input:

  • Uncontrolled (không kiểm soát): không có prop value — DOM tự quản lý state.
  • Controlled (có kiểm soát): value gắn với React state — React là nguồn dữ liệu duy nhất.

Cảnh báo xuất hiện khi một input bắt đầu ở trạng thái uncontrolled (value={undefined}), rồi chuyển sang controlled (value="something") sau khi state cập nhật. React không thể xử lý sự thay đổi danh tính đó giữa vòng đời component.

Chín trong mười trường hợp, nguyên nhân rất đơn giản: state được khởi tạo là undefined hoặc null thay vì chuỗi rỗng.

Cách khắc phục từng bước

Bước 1 — Tìm input bị lỗi

Cảnh báo thường chỉ rõ tên component. Nếu không, mở React DevTools và tìm các input có value chuyển đổi giữa undefined và chuỗi thực qua các lần render. Bắt đầu từ phần khởi tạo state — đó là nơi xảy ra vấn đề trong 90% trường hợp.

Bước 2 — Khởi tạo state bằng chuỗi rỗng

Chỉ một thay đổi này là đủ để sửa phần lớn các trường hợp. Đây là pattern bị lỗi:

// BROKEN — value starts as undefined
const [name, setName] = useState();
const [email, setEmail] = useState(null);

return (
  <form>
    <input type="text" value={name} onChange={e => setName(e.target.value)} />
    <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
  </form>
);

Và đây là cách sửa:

// FIXED — value starts as empty string
const [name, setName] = useState('');
const [email, setEmail] = useState('');

return (
  <form>
    <input type="text" value={name} onChange={e => setName(e.target.value)} />
    <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
  </form>
);

Bước 3 — Sửa state dạng object cho form

Nhiều form lưu tất cả các trường trong một object state duy nhất. Khi làm vậy, mỗi trường cần có giá trị mặc định là chuỗi rõ ràng — các trường bị thiếu sẽ ngầm trở thành undefined:

// BROKEN — form.email is undefined → uncontrolled on first render
const [form, setForm] = useState({ name: '' });

// FIXED — every field declared upfront
const [form, setForm] = useState({
  name: '',
  email: '',
  phone: '',
  message: ''
});

const handleChange = (e) => {
  setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};

Bước 4 — Xử lý dữ liệu từ API

Khi fetch dữ liệu người dùng để điền vào form, bạn gặp vấn đề về thời điểm. Trong khi request đang chạy, usernull — nghĩa là mọi giá trị input đều là undefined ở lần render đầu tiên:

// BROKEN — user?.name is undefined while fetching
const [user, setUser] = useState(null);

useEffect(() => {
  fetchUser().then(data => setUser(data));
}, []);

return (
  <input type="text" value={user?.name} onChange={...} />
);

// FIXED — nullish coalescing as a safety net
return (
  <input type="text" value={user?.name ?? ''} onChange={...} />
);

Tốt hơn nữa, hãy khởi tạo user với đầy đủ cấu trúc ngay từ đầu để không cần fallback:

const [user, setUser] = useState({ name: '', email: '', role: '' });

useEffect(() => {
  fetchUser().then(data => setUser(data));
}, []);

Bước 5 — Không dùng lẫn value và defaultValue

Với input uncontrolled cần giá trị ban đầu, hãy dùng defaultValue. Dùng cả value lẫn defaultValue trên cùng một input là lỗi thường gặp:

// Mixing controlled and uncontrolled — pick one
<input value={name} defaultValue="John" onChange={...} />

// Controlled — state drives everything
<input type="text" value={name} onChange={e => setName(e.target.value)} />

// Uncontrolled — DOM manages value, read it via ref
<input type="text" defaultValue="John" ref={inputRef} />

Kiểm tra sau khi sửa

  • Mở Chrome DevTools → tab Console.
  • Tải lại trang và tương tác với form: gõ, submit, xóa một trường.
  • Cảnh báo A component is changing an uncontrolled input phải biến mất.
  • Trong React DevTools, kiểm tra component — prop value của input phải luôn là chuỗi. Không bao giờ là undefined, không bao giờ là null.

Vẫn còn cảnh báo? Mở rộng phạm vi tìm kiếm. Tìm toàn bộ cây component với các phần tử <input>, <textarea>, và <select> — bất kỳ phần tử nào cũng có thể gây ra cảnh báo này.

Danh sách kiểm tra nhanh

  • useState() không có đối số → đổi thành useState('')
  • useState(null) cho trường text → đổi thành useState('')
  • Object state thiếu trường → khai báo tất cả các trường với giá trị mặc định ''
  • Dữ liệu API dùng trực tiếp làm value → thêm fallback ?? ''
  • Cả valuedefaultValue trên cùng một input → xóa một trong hai
  • Checkbox hoặc radio? Dùng checked với giá trị mặc định false, không phải undefined

Trường hợp đặc biệt: Checkbox

Cảnh báo tương tự cũng xảy ra với checkbox khi checked bắt đầu là undefined. Cách sửa giống nhau, chỉ khác giá trị mặc định:

// BROKEN
const [agreed, setAgreed] = useState(); // undefined
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />

// FIXED
const [agreed, setAgreed] = useState(false);
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />

Nguyên tắc cốt lõi: một khi bạn đặt prop value hoặc checked lên một input, React sẽ kiểm soát nó. Điều đó có nghĩa là state của React phải là giá trị hợp lệ ngay từ lần render đầu tiên — chuỗi rỗng cho trường text, false cho checkbox. undefinednull sẽ trả quyền kiểm soát lại cho DOM, và đó chính xác là lúc mọi rắc rối bắt đầu.

Related Error Notes