📝 Form và controlled components

By Better Wise

Vì sao form là chỗ dễ sai nhất khi mới học React

Ba module trước bạn đã quen với useState và xử lý sự kiện: bấm nút thì count tăng, React render lại. Form là bài kiểm tra thật sự cho tư duy đó, vì người dùng gõ từng ký tự, và React phải là nơi giữ giá trị thay vì DOM.

Sau bài này bạn sẽ:

  • Chuyển một input HTML quen thuộc thành controlled input.
  • Quản lý nhiều trường trong một object state duy nhất.
  • Xử lý submit đúng cách bằng preventDefault.
  • Validate cơ bản và hiển thị lỗi dưới từng trường.

Từ input HTML quen thuộc đến controlled component

Bạn đã viết thế này nhiều lần:

<input type="text" placeholder="Tên ghi chú" />

Trình duyệt tự giữ giá trị trong DOM, React không biết gì cả. Đó là uncontrolled. Muốn React làm chủ, ta nối hai chiều lại:

const [title, setTitle] = useState("");

<input
  value={title}
  onChange={(e) => setTitle(e.target.value)}
/>

Một input là controlled khi giá trị của nó đến từ _____ và thay đổi qua sự kiện _____. Vòng lặp này chạy liên tục mỗi lần gõ:

flowchart TD
  A["⌨️ Người dùng gõ"] --> B["onChange chạy"]
  B --> C["setTitle với giá trị mới"]
  C --> D["React render lại"]
  D --> E["input nhận value mới"]
  E --> A
Input HTML thường Controlled input
Ai giữ giá trị DOM State của React
Đọc giá trị document.querySelector Biến state
Validate khi gõ Khó Dễ, chỉ cần if

Vì sao phải làm khổ vậy? Vì khi giá trị nằm trong state, bạn có thể validate ngay, disable nút khi chưa hợp lệ, hay cho hai trường phụ thuộc nhau. Đó chính là lý do form trong React gần như luôn là controlled.

Note: Nếu value ban đầu là undefined, React sẽ cảnh báo "changing an uncontrolled input to be controlled". Luôn khởi tạo bằng chuỗi rỗng useState("").

Bài tập nhỏ: thêm một input email vào form ghi chú và biến nó thành controlled.

Xem gợi ý

const [email, setEmail] = useState("");

<input
  type="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>

Quản lý nhiều trường bằng một object state

Mỗi trường một useState thì form 5 trường thành 10 dòng khai báo. Cách gọn hơn là gom vào một object:

const [form, setForm] = useState({
  title: "",
  category: "hoc-tap",
  important: false,
});

function handleChange(event) {
  const { name, value, type, checked } = event.target;
  setForm((prev) => ({
    ...prev,
    [name]: type === "checkbox" ? checked : value,
  }));
}

Ba điểm cần nhớ:

  • Mỗi input phải có name khớp với key trong state.
  • [name] là computed property, nhờ đó một handler dùng cho mọi trường.
  • Với checkbox, đọc _____ thay vì value.
  • Khi cập nhật một trường, dùng _____ để giữ các trường còn lại.

Và hai cái bẫy HTML quen thuộc:

// textarea: dùng value, không dùng children
<textarea value={form.title} name="title" onChange={handleChange} />

// select: đặt value trên select, không đặt selected trên option
<select value={form.category} name="category" onChange={handleChange}>
  <option value="hoc-tap">Học tập</option>
  <option value="ca-nhan">Cá nhân</option>
</select>

Bài tập nhỏ: thêm trường checkbox "quan trọng" và in ra giá trị trong console.log.

Xem gợi ý

<input
  type="checkbox"
  name="important"
  checked={form.important}
  onChange={handleChange}
/>

Handler chung đã xử lý type === "checkbox" nên không cần viết thêm gì.

Xử lý submit đúng cách

Trong HTML thuần, bấm nút submit là trình duyệt gửi form và tải lại trang. Trong React, việc tải lại đó xóa sạch state, nên ta chặn nó:

function handleSubmit(event) {
  event.preventDefault();

  const errors = validate(form);
  if (Object.keys(errors).length > 0) {
    setErrors(errors);
    return;
  }

  onAdd(form);          // báo cho component cha
  setForm({ title: "", category: "hoc-tap", important: false });
}

Gọi _____ trong handleSubmit là bước không được quên. Nút submit cần type="submit" để form kích hoạt onSubmit; nút phụ (như "Hủy") nên để type="button", nếu không nó cũng submit.

Validate cơ bản ngay khi nhập

Validate trong React rất nhẹ: một hàm thuần nhận form và trả về object lỗi.

function validate(values) {
  const errors = {};
  if (values.title.trim().length < 3) {
    errors.title = "Tên ghi chú cần ít nhất 3 ký tự";
  }
  return errors;
}

Object lỗi rỗng nghĩa là hợp lệ, đó là toàn bộ giao kèo. Cách hiển thị:

{errors.title && <p className="error">{errors.title}</p>}

Một mẹo nhỏ về trải nghiệm: chỉ hiện lỗi sau khi người dùng đã gõ hoặc đã bấm submit, đừng hiện lỗi đỏ ngay khi form vừa mở.

Bài tập nhỏ: bắt buộc người dùng chọn category, hiện lỗi dưới select nếu để trống.

Xem gợi ý

Đổi option đầu thành <option value="">Chọn danh mục</option>, thêm vào validate:

if (!values.category) {
  errors.category = "Vui lòng chọn danh mục";
}

Rồi render {errors.category && <p className="error">{errors.category}</p>} ngay dưới select.

Front: Controlled component là gì? Back: Component mà giá trị hiển thị của input do state React quyết định và cập nhật qua onChange.

Front: Hai props bắt buộc để một input thành controlled? Back: value và onChange.

Front: Vì sao form React bị tải lại trang khi submit? Back: Đó là hành vi mặc định của trình duyệt, cần gọi event.preventDefault().

Front: Khi nào React cảnh báo chuyển từ uncontrolled sang controlled? Back: Khi value ban đầu là undefined rồi sau đó thành chuỗi, nên khởi tạo state bằng "".

Front: Vì sao không được gán form.title = 'abc' trực tiếp? Back: Đó là mutate state, React không render lại; phải dùng setForm với object mới.

Front: Một handler dùng cho nhiều input lấy tên trường từ đâu? Back: Từ event.target.name, dùng computed property [name].

Front: Với checkbox, đọc giá trị ở đâu? Back: event.target.checked, là boolean, không phải value.

Front: Textarea trong React nhận nội dung qua đâu? Back: Qua prop value, không dùng children như HTML thuần.

Front: Value của select đặt ở đâu trong React? Back: Trên thẻ select, không đặt selected trên từng option.

Front: Làm sao cập nhật một trường mà giữ nguyên các trường khác? Back: setForm(prev => ({ ...prev, [name]: value })).

Front: Hàm validate nên trả về gì? Back: Một object lỗi với key là tên trường; object rỗng nghĩa là hợp lệ.

Front: Khi nào nên dùng controlled thay vì uncontrolled? Back: Khi cần validate ngay, disable nút theo giá trị, hoặc các trường phụ thuộc nhau.

Bốn lỗi form kinh điển của người mới

  • Quên name: handler chung chạy nhưng [name] thành undefined, state không đổi.
  • Quên preventDefault: trang tải lại, bạn tưởng state bị mất.
  • Đặt selected trên option hoặc viết <textarea>{value}</textarea>: React cảnh báo, hãy dùng value trên select và trên textarea.
  • Validate chỉ bằng cách disable nút: người dùng vẫn submit được bằng Enter; luôn validate lại trong handleSubmit.

Một quy tắc nhanh: nếu bạn thấy mình đang dùng document.querySelector hay ref để đọc giá trị input, hãy dừng lại và hỏi giá trị đó nên nằm trong state nào.

Ghi nhớ nhanh và bước tiếp theo

  • Controlled input = value từ state + onChange cập nhật state.
  • Gom nhiều trường vào một object, dùng name và spread để cập nhật gọn.
  • event.preventDefault() trong onSubmit, validate bằng một hàm thuần trả về object lỗi.
  • Checkbox dùng checked, textarea và select dùng value.

Form của bạn giờ đã có dữ liệu sạch trong state. Bài tiếp theo, useEffect và side effects, sẽ trả lời câu hỏi: làm sao chạy code sau khi component render, và làm sao gọi API đúng lúc thay vì gọi lung tung mỗi lần gõ phím.