Bài 17.2: Controlled components với React-TypeScript

Xử lý các input trong form là một công việc _không thể thiếu_ khi xây dựng bất kỳ ứng dụng web tương tác nào. Làm sao để lấy được dữ liệu người dùng nhập vào? Làm sao để hiển thị dữ liệu đó ngay lập tức khi họ gõ? Làm sao để thực hiện validation? Trong React, chúng ta có một cách tiếp cận mạnh mẽ và được khuyến khích để giải quyết những vấn đề này: sử dụng Controlled Components.

Và khi kết hợp với sức mạnh kiểm soát kiểu dữ liệu của TypeScript, việc làm việc với form càng trở nên an toàn và dễ quản lý hơn bao giờ hết!

Controlled Components là gì?

Trong HTML truyền thống, các phần tử form như <input>, <textarea>, <select> thường tự quản lý trạng thái (state) của riêng chúng. Tức là, giá trị hiện tại của ô input nằm _trong_ DOM.

Với Controlled Components trong React, mọi thứ khác đi một chút. Thay vì để DOM "tự lo", chúng ta sẽ biến state của component React trở thành "nguồn sự thật duy nhất" (single source of truth) cho giá trị của phần tử form. Giá trị hiển thị trên UI luôn luôn được điều khiển bởi state của component.

Hãy hình dung thế này: Bạn có một biến state trong component. Giá trị của biến state này sẽ được gán vào thuộc tính value của input. Bất cứ khi nào người dùng gõ chữ, thay vì input tự thay đổi giá trị nội tại, chúng ta sẽ bắt sự kiện onChange và dùng nó để cập nhật state. Khi state được cập nhật, React sẽ re-render component, và input lại nhận giá trị mới từ state thông qua thuộc tính value. Đó là một vòng lặp khép kín và hoàn toàn do React kiểm soát.

Cơ chế hoạt động: valueonChange

Bí quyết của Controlled Components nằm ở việc sử dụng hai thuộc tính chính trên các phần tử form:

  1. value: Thuộc tính này nhận giá trị từ state của component. Nó buộc phần tử form hiển thị chính xác giá trị mà state đang giữ.
  2. onChange: Thuộc tính này là một event handler (hàm xử lý sự kiện) được gọi mỗi khi giá trị của phần tử form thay đổi (ví dụ: người dùng gõ một ký tự, chọn một option trong select). Bên trong handler này, chúng ta sẽ lấy giá trị mới từ sự kiện và dùng hàm cập nhật state (setState hoặc từ useState) để thay đổi state của component.

Đây chính là vòng lặp thần kỳ: State -> value -> Thay đổi input -> onChange -> Cập nhật State -> re-render -> State mới -> value mới...

Bắt đầu với TypeScript: Input đơn giản

Kết hợp TypeScript vào đây giúp chúng ta định nghĩa rõ ràng kiểu dữ liệu cho state và cho các event handler. Hãy xem ví dụ về một ô input đơn giản để nhập tên:

import React, { useState, ChangeEvent } from 'react';

function NameInput() {
  // 1. Định nghĩa state với kiểu string
  const [name, setName] = useState<string>('');

  // 2. Định nghĩa hàm xử lý sự kiện onChange với kiểu ChangeEvent<HTMLInputElement>
  const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
    // Lấy giá trị mới từ input và cập nhật state
    setName(event.target.value);
  };

  return (
    <div>
      <label htmlFor="name">Nhập tên của bạn:</label>
      <input
        type="text"
        id="name"
        value={name} // 3. Gán giá trị state vào thuộc tính 'value'
        onChange={handleNameChange} // 4. Gắn hàm xử lý sự kiện vào thuộc tính 'onChange'
      />
      {/* Hiển thị giá trị hiện tại của state ngay lập tức */}
      <p>Tên bạn đã nhập: **{name}**</p>
    </div>
  );
}

export default NameInput;

Giải thích:

  • useState<string>(''): Chúng ta khai báo một state name với giá trị ban đầu là chuỗi rỗng (''). TypeScript biết name sẽ luôn là kiểu string.
  • ChangeEvent<HTMLInputElement>: Đây là kiểu dữ liệu mà TypeScript cung cấp cho sự kiện change trên các phần tử <input>. Nó đảm bảo rằng trong hàm handleNameChange, đối tượng event sẽ có đầy đủ các thuộc tính cần thiết của một sự kiện trên input, bao gồm event.target.value.
  • event.target.value: Thuộc tính này chứa giá trị mới nhất của ô input sau khi sự kiện change xảy ra.
  • setName(event.target.value): Chúng ta gọi hàm cập nhật state để thay đổi giá trị của name.
  • value={name}: Thuộc tính value của thẻ <input> luôn nhận giá trị từ state name. Điều này đảm bảo input luôn hiển thị đúng giá trị mà state đang giữ.
  • onChange={handleNameChange}: Mỗi khi người dùng gõ phím, hàm handleNameChange sẽ được gọi.

Bạn có thể thấy dòng <p>Tên bạn đã nhập: **{name}**</p> sẽ cập nhật nội dung ngay lập tức khi bạn gõ. Đó là sức mạnh của việc state là nguồn sự thật!

Áp dụng cho các loại phần tử form khác

Cơ chế valueonChange tương tự cũng được áp dụng cho <textarea><select>, chỉ khác một chút về kiểu dữ liệu của sự kiện onChange.

  • <textarea>: Hoạt động y hệt <input text>. Bạn sử dụng thuộc tính valueonChange. Kiểu sự kiện là ChangeEvent<HTMLTextAreaElement>.

    import React, { useState, ChangeEvent } from 'react';
    
    function DescriptionInput() {
      const [description, setDescription] = useState<string>('');
    
      const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
        setDescription(event.target.value);
      };
    
      return (
        <div>
          <label htmlFor="description"> tả:</label>
          <textarea
            id="description"
            value={description}
            onChange={handleDescriptionChange}
          />
          <p> tả của bạn: *{description}*</p>
        </div>
      );
    }
    
  • <select>: Cũng sử dụng thuộc tính valueonChange. Giá trị của <select> controlled component là thuộc tính value trên thẻ <select> chính (không phải trên các thẻ <option>). Để lấy giá trị option được chọn, bạn vẫn truy cập event.target.value. Kiểu sự kiện là ChangeEvent<HTMLSelectElement>.

    import React, { useState, ChangeEvent } from 'react';
    
    function SelectColor() {
      const [selectedColor, setSelectedColor] = useState<string>('red'); // Giá trị ban đầu
    
      const handleColorChange = (event: ChangeEvent<HTMLSelectElement>) => {
        setSelectedColor(event.target.value);
      };
    
      return (
        <div>
          <label htmlFor="color">Chọn màu:</label>
          <select id="color" value={selectedColor} onChange={handleColorChange}>
            <option value="red">Đỏ</option>
            <option value="blue">Xanh Dương</option>
            <option value="green">Xanh </option>
          </select>
          <p>Màu bạn chọn: **{selectedColor}**</p>
        </div>
      );
    }
    

    Lưu ý: Với <select multiple>, giá trị của event.target.value sẽ là một mảng các chuỗi.

Xử lý nhiều Inputs với một State Object

Trong các form thực tế, bạn thường có nhiều hơn một ô input. Việc tạo riêng từng useState cho mỗi input có thể dẫn đến boilerplate code khá nhiều. Một cách tiếp cận thông minh và gọn gàng hơn là sử dụng một object làm state để chứa tất cả các giá trị của form, và viết một hàm xử lý sự kiện onChange chung cho tất cả các input.

Để làm được điều này, mỗi thẻ <input> hoặc <textarea> cần có thêm thuộc tính name để hàm xử lý chung biết được field nào đang thay đổi.

import React, { useState, ChangeEvent, FormEvent } from 'react';

// Định nghĩa kiểu dữ liệu cho state object
interface FormData {
  firstName: string;
  lastName: string;
  email: string;
  description: string; // Ví dụ thêm textarea
}

function MultiInputForm() {
  // State là một object chứa tất cả giá trị form
  const [formData, setFormData] = useState<FormData>({
    firstName: '',
    lastName: '',
    email: '',
    description: '',
  });

  // Handler chung cho cả input và textarea
  // Sử dụng union type cho ChangeEvent
  const handleInputChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value } = event.target; // Lấy thuộc tính 'name' và 'value' từ phần tử gây ra sự kiện

    // Cập nhật state object, giữ lại các field khác
    setFormData(prevState => ({
      ...prevState, // Copy tất cả các thuộc tính hiện có
      [name]: value, // Cập nhật thuộc tính có tên trùng với 'name' của input
    }));
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault(); // Ngăn hành vi submit form mặc định (reload trang)
    console.log('Dữ liệu form:', formData);
    // Tại đây bạn có thể xử lý dữ liệu formData, ví dụ gửi lên API
    // reset form nếu cần: setFormData({ firstName: '', lastName: '', email: '', description: '' });
  };

  return (
    <form onSubmit={handleSubmit}> {/* Gắn handler cho sự kiện submit */}
      <div>
        <label htmlFor="firstName">Tên:</label>
        <input
          type="text"
          id="firstName"
          name="firstName" // **Quan trọng:** Thêm thuộc tính 'name'
          value={formData.firstName} // Gán giá trị từ state object
          onChange={handleInputChange} // Sử dụng handler chung
        />
      </div>
      <div>
        <label htmlFor="lastName">Họ:</label>
        <input
          type="text"
          id="lastName"
          name="lastName" // **Quan trọng:** Thêm thuộc tính 'name'
          value={formData.lastName}
          onChange={handleInputChange}
        />
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email" // **Quan trọng:** Thêm thuộc tính 'name'
          value={formData.email}
          onChange={handleInputChange}
        />
      </div>
      <div>
        <label htmlFor="description"> tả chi tiết:</label>
        <textarea
          id="description"
          name="description" // **Quan trọng:** Thêm thuộc tính 'name'
          value={formData.description}
          onChange={handleInputChange}
        />
      </div>
      <button type="submit">Gửi Form</button>
    </form>
  );
}

export default MultiInputForm;

Giải thích:

  • interface FormData: Chúng ta định nghĩa kiểu cho object state để TypeScript biết cấu trúc dữ liệu.
  • useState<FormData>({...}): State formData bây giờ là một object tuân theo kiểu FormData.
  • ChangeEvent<HTMLInputElement | HTMLTextAreaElement>: Kiểu này cho phép handler handleInputChange nhận sự kiện từ cả <input><textarea>.
  • const { name, value } = event.target;: Đây là kỹ thuật destructuring để lấy ra thuộc tính name (tên của input/textarea, ví dụ: "firstName", "email") và value (giá trị hiện tại của input/textarea) từ phần tử DOM gây ra sự kiện.
  • setFormData(prevState => ({ ...prevState, [name]: value, }));: Đây là phần mấu chốt. Chúng ta sử dụng functional update của state (prevState => ...) để đảm bảo luôn làm việc với state gần nhất. { ...prevState } tạo một bản sao (shallow copy) của state object trước đó. [name]: value là cú pháp computed property name của JavaScript, nó sẽ thêm hoặc cập nhật thuộc tính có tên bằng giá trị của biến name (ví dụ: nếu name là "email", nó sẽ cập nhật thuộc tính email).
  • name="firstName" (trên các thẻ input/textarea): Thuộc tính này là bắt buộc để hàm xử lý chung handleInputChange có thể biết được thuộc tính nào trong state object cần được cập nhật.
  • onSubmit={handleSubmit} trên thẻ <form>: Gắn hàm xử lý sự kiện submit form.
  • event.preventDefault(): Quan trọng! Nó ngăn trình duyệt thực hiện hành vi submit form mặc định (thường là tải lại trang), cho phép React kiểm soát việc submit.

Với cách này, bạn có thể dễ dàng thêm bao nhiêu input tùy ý vào form mà chỉ cần sửa đổi interface FormData và thêm input tương ứng vào JSX, chỉ cần đảm bảo thuộc tính name khớp với tên thuộc tính trong state object.

Ưu điểm của Controlled Components

Việc sử dụng Controlled Components mang lại rất nhiều lợi ích:

  • State luôn được đồng bộ: Giá trị trong state React luôn phản ánh chính xác những gì người dùng nhìn thấy và nhập vào trên UI.
  • Truy cập giá trị dễ dàng: Bạn luôn có thể truy cập giá trị hiện tại của form thông qua state bất cứ lúc nào.
  • Validation theo thời gian thực: Rất dễ dàng để kiểm tra giá trị nhập vào và hiển thị lỗi ngay khi người dùng gõ.
  • Chuyển đổi/Định dạng dữ liệu: Bạn có thể xử lý (ví dụ: chuyển sang chữ in hoa, loại bỏ ký tự đặc biệt) giá trị ngay trong handler onChange trước khi cập nhật state.
  • Điều khiển trạng thái UI: Dễ dàng bật/tắt (enable/disable) các phần tử khác trong form hoặc trên trang dựa trên giá trị của input (ví dụ: disable nút "Gửi" cho đến khi email hợp lệ).
  • Debugging đơn giản hơn: Vì state là nguồn sự thật, việc theo dõi luồng dữ liệu và tìm lỗi trở nên dễ dàng hơn nhiều.
  • An toàn với TypeScript: TypeScript giúp bạn đảm bảo rằng bạn đang xử lý đúng kiểu dữ liệu cho state và sự kiện, giảm thiểu lỗi runtime.

Comments

There are no comments at the moment.