Skip to content

Controlled vs Uncontrolled Input trong React

Và 3 cách collect form data từ "ngây ngô" đến "pro max".
Hãy tưởng tượng, bạn đang xây dựng form trong React.
Gõ vào input thì hiển thị bình thường, submit thì lấy được data… Mọi thứ có vẻ ổn.
Nhưng rồi bạn thêm vài field, thêm vài state, mỗi cái lại một useState, và bỗng dưng component của bạn trông như thế này:
const [title, setTitle] = useState('');
const [category, setCategory] = useState('Work');
const [priority, setPriority] = useState('Medium');
const [description, setDescription] = useState('');
const [dueDate, setDueDate] = useState('');
const [assignee, setAssignee] = useState('');
// ...còn nữa
Có gì đó không ổn. Và đúng là không ổn thật. Trước khi đến cách làm tốt hơn, hãy hiểu rõ thứ nền tảng nhất: Controlled vs Uncontrolled Input.

Controlled Input là gì?

Controlled Input là input mà giá trị của nó được React kiểm soát hoàn toàn thông qua state.
const [title, setTitle] = useState('');

<input
name="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
Mỗi lần bạn gõ một ký tự:
onChange được gọi
setTitle cập nhật state
React re-render, input hiển thị giá trị mới từ state
React là nguồn sự thật duy nhất (single source of truth). DOM không tự quản lý gì cả — nó chỉ hiển thị những gì React bảo.
Nếu bạn thêm value={title} nhưng quên onChange, React sẽ cảnh báo ngay:
You provided a `value` prop to a form field without an `onChange` handler.
This will render a read-only field.
Lúc này input sẽ bị “đóng băng” — bạn gõ không có gì xảy ra cả.
Nếu thực sự muốn read-only → thêm readOnly prop:
<input value={title} readOnly />
Lúc này React không cảnh báo, và rõ ràng về ý định: "input này chỉ để hiển thị, không cho sửa."

Uncontrolled Input là gì?

Uncontrolled Input là input mà trình duyệt tự quản lý giá trị, React không can thiệp trực tiếp.
<input
name="title"
onChange={handleChange}
type="text"
placeholder="Title"
/>
Không có value={...} → đây là uncontrolled input. Trình duyệt tự hiển thị những gì bạn gõ. React chỉ “biết” về giá trị khi bạn hỏi nó — qua e.target.value trong onChange, hoặc qua ref.
Nhưng tại sao vẫn submit được bình thường?
Vì khi bạn có onChange handler cập nhật state, thực chất bạn đang “nghe lén” DOM rồi lưu vào state thủ công. Khi submit, bạn đọc từ state đó — và mọi thứ vẫn hoạt động.
Controlled
Uncontrolled
Ai quản lý giá trị?
React state
DOM (trình duyệt)
Cách đọc giá trị
Từ state
e.target.value hoặc ref
Phù hợp khi
Cần validate real-time, disable button…
Form đơn giản, ít tương tác

3 cách collect form data — từ ngây ngô đến pro

Cách 1: HTML form submit thuần (DOM)

Đây là cách “ngây ngô” nhất — không dùng state, để form HTML tự xử lý.
function AddTaskForm() {
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const title = form.title.value;
const category = form.category.value;
console.log({ title, category });
};

return (
<form onSubmit={handleSubmit}>
<input name="title" type="text" placeholder="Title" />
<input name="category" type="text" placeholder="Category" />
<button type="submit">Add Task</button>
</form>
);
}
Bạn đọc giá trị trực tiếp từ DOM thông qua e.target.fieldName.value. Không cần state, không cần onChange.
Cách này cũng dùng được với FormData:
const handleSubmit = (e) => {
e.preventDefault();
const data = new FormData(e.target);
const title = data.get('title');
const category = data.get('category');
};
Hoặc dùng ref — cũng là uncontrolled, nhưng đọc giá trị qua useRef thay vì e.target:
import { useRef } from 'react';

function AddTaskForm() {
const titleRef = useRef(null);

const handleSubmit = (e) => {
e.preventDefault();
console.log(titleRef.current.value);
};

return (
<form onSubmit={handleSubmit}>
<input ref={titleRef} type="text" name="title" placeholder="Title" />
<button type="submit">Add Task</button>
</form>
);
}
useRef tạo ra object { current: null }, khi React mount input vào DOM thì tự gán DOM element vào titleRef.current. Từ đó bạn đọc .value bất cứ lúc nào mà không cần onChange, không cần state, không trigger re-render.
Khi nào dùng? Form cực kỳ đơn giản, không cần validate real-time, không cần disable/enable button theo state. Hiếm gặp trong React app thực tế.

Cách 2: Mỗi field một useState (pro thường)

Đây là cách nhiều người học React làm đầu tiên khi cần controlled inputs.
function AddTaskForm() {
const [title, setTitle] = useState('');
const [category, setCategory] = useState('Work');
const [priority, setPriority] = useState('Medium');
const [description, setDescription] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
console.log({ title, category, priority, description });
};

return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<input
type="text"
value={category}
onChange={(e) => setCategory(e.target.value)}
placeholder="Category"
/>
{/* ...các field khác */}
<button type="submit">Add Task</button>
</form>
);
}
Mỗi input có state riêng, handler riêng. Rõ ràng, dễ đọc. Nhưng khi form có 6–8 field, bạn sẽ thấy component phình to nhanh chóng.

Cách 3: Dùng một formData object (pro hơn)

Thay vì nhiều state riêng lẻ, gom tất cả vào một state object duy nhất:
function AddTaskForm() {
const [formData, setFormData] = useState({
title: '',
category: 'Work',
priority: 'Medium',
description: '',
});

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

const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.