React Roadmap: Xử Lý Form Gọn Gàng Với React Hook Form

Chào mừng các bạn quay trở lại với series “React Roadmap – Lộ trình học React 2025“! Sau khi đã cùng nhau khám phá những viên gạch nền tảng của React như React là gì, cách làm việc với Props và State, hay sức mạnh của Hooks như useState và useEffect, chúng ta đã có đủ công cụ để xây dựng các component tương tác. Hôm nay, chúng ta sẽ đi sâu vào một khía cạnh cực kỳ phổ biến và không kém phần thách thức trong phát triển web: Xử lý Form.

Form là trái tim của rất nhiều ứng dụng web, từ đăng nhập, đăng ký, tìm kiếm cho đến các biểu mẫu phức tạp hơn như đặt hàng, khảo sát. Xử lý trạng thái (state) của form, xác thực dữ liệu (validation), và gửi dữ liệu đi có thể nhanh chóng trở nên cồng kềnh, lặp đi lặp lại (boilerplate), và gây ra hiệu năng kém nếu không được thực hiện đúng cách.

Nếu bạn đã từng thử quản lý state cho từng input field bằng `useState` và cảm thấy code của mình phình to một cách mất kiểm soát, đặc biệt là khi có validation, thì bạn không đơn độc. May mắn thay, cộng đồng React đã tạo ra những thư viện tuyệt vời để giải quyết vấn đề này. Trong số đó, React Hook Form nổi lên như một lựa chọn hiện đại, hiệu quả và mang lại trải nghiệm phát triển (developer experience) cực kỳ tốt.

Bài viết này sẽ hướng dẫn bạn cách xử lý form một cách “sạch sẽ” (clean way) bằng React Hook Form. Chúng ta sẽ cùng tìm hiểu tại sao nó lại là lựa chọn ưu việt, cách bắt đầu, các tính năng cốt lõi như đăng ký input, xử lý submission, và validation. Hãy cùng nhau biến nỗi ám ảnh về form thành một trải nghiệm thú vị nhé!

Tại Sao Nên Chọn React Hook Form?

Trước khi đi sâu vào cách sử dụng, hãy điểm qua những lợi ích nổi bật khiến React Hook Form (RHF) trở thành một công cụ mạnh mẽ cho việc quản lý form trong React, đặc biệt khi so sánh với các phương pháp truyền thống hoặc các thư viện khác như Formik:

  1. Hiệu Năng Vượt Trội (Performance): Đây là lợi thế lớn nhất của RHF. Nó sử dụng khái niệm uncontrolled inputs (input không hoàn toàn được quản lý state bởi React component) bằng cách làm việc trực tiếp với DOM references (Refs). Điều này giúp giảm thiểu đáng kể số lần component bị re-render khi người dùng gõ vào input. Với các form lớn và phức tạp, việc này tạo ra sự khác biệt rõ rệt về tốc độ phản hồi.
  2. Giảm Boilerplate Code: So với việc khai báo `useState` cho từng trường input và viết hàm `onChange` riêng, RHF cung cấp API đơn giản và trực quan hơn rất nhiều thông qua hook `useForm`. Code của bạn sẽ trở nên gọn gàng và dễ đọc hơn.
  3. Dễ Dàng Validation: RHF tích hợp sẵn các quy tắc validation phổ biến và cho phép bạn dễ dàng định nghĩa các quy tắc tùy chỉnh. Việc hiển thị thông báo lỗi cũng rất thuận tiện thông qua object `errors`. Nó cũng hỗ trợ tích hợp mượt mà với các thư viện schema validation nổi tiếng như Yup, Zod, Joi cho các form phức tạp.
  4. Kích Thước Nhỏ (Small Bundle Size): RHF được thiết kế với kích thước nhỏ, giúp giảm tổng dung lượng file JavaScript mà người dùng cần tải về, từ đó cải thiện thời gian tải trang.
  5. Dựa Trên Hooks (Hook-Based): Được xây dựng hoàn toàn dựa trên React Hooks, RHF hoàn toàn phù hợp với phong cách lập trình Functional Components hiện đại mà chúng ta đã thảo luận trong bài viết về Class vs Functional Components.

Bắt Đầu Với React Hook Form

Việc cài đặt React Hook Form rất đơn giản. Mở terminal trong project React của bạn và chạy lệnh:


npm install react-hook-form
# hoặc
yarn add react-hook-form

Sau khi cài đặt, bạn đã sẵn sàng sử dụng hook `useForm` trong functional component của mình.

Sử Dụng `useForm` và `register`

Trái tim của React Hook Form là hook `useForm()`. Khi gọi hook này, nó trả về một đối tượng chứa nhiều phương thức và trạng thái cần thiết để quản lý form. Hai thứ quan trọng nhất bạn sẽ sử dụng ban đầu là `register` và `handleSubmit`.

  • `register`: Là một function dùng để “đăng ký” input field vào React Hook Form. Khi bạn gọi `register` với tên của input (ví dụ: `”email”`, `”password”`), RHF sẽ gắn các thuộc tính cần thiết (bao gồm cả một ref) vào input đó để theo dõi giá trị của nó mà không cần bạn phải quản lý state thủ công bằng `useState`.
  • `handleSubmit`: Là một function dùng để bọc (wrap) hàm xử lý submit form của bạn. Nó sẽ xử lý quá trình lấy dữ liệu, chạy validation, và chỉ gọi hàm callback của bạn khi form hợp lệ.

Hãy xem một ví dụ cơ bản:


import { useForm } from 'react-hook-form';

function SimpleForm() {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    // 'data' là một object chứa giá trị của form
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h3>Form Đăng ký Đơn giản</h3>
      <div>
        <label>Tên đăng nhập:</label>
        {/* Sử dụng spread operator để gắn các props từ register */}
        <input type="text" {...register("username")} />
      </div>
      <div>
        <label>Mật khẩu:</label>
        <input type="password" {...register("password")} />
      </div>
      <button type="submit">Đăng ký</button>
    </form>
  );
}

// Render component này trong ứng dụng của bạn
// ReactDOM.render(<SimpleForm />, document.getElementById('root'));

Trong ví dụ trên:

  • Chúng ta gọi `useForm()` để lấy `register` và `handleSubmit`.
  • Trong hàm `onSubmit`, đối số `data` chứa một object mà key là tên của các input đã được `register`, và value là giá trị hiện tại của input đó.
  • Sự kiện `onSubmit` của form được gán bằng `handleSubmit(onSubmit)`. RHF sẽ chặn sự kiện submit mặc định của trình duyệt, chạy validation (nếu có), và chỉ gọi hàm `onSubmit` của chúng ta khi mọi thứ hợp lệ.
  • Đối với mỗi input, chúng ta gọi `register(“tên_input”)` và sử dụng spread operator (`{…}`) để truyền tất cả các props mà `register` trả về vào thẻ input. Điều này rất quan trọng vì RHF cần các props này (bao gồm cả `ref`) để theo dõi giá trị và trạng thái của input.

Với cách này, bạn không cần viết hàm `onChange` riêng cho từng input, không cần quản lý state bằng `useState` cho từng trường. RHF đã làm tất cả cho bạn một cách hiệu quả ở hậu trường.

Validation Với React Hook Form

Form thường đi kèm với yêu cầu validation để đảm bảo dữ liệu người dùng nhập vào là hợp lệ. React Hook Form cung cấp các quy tắc validation được tích hợp sẵn ngay trong hàm `register`.

Các quy tắc validation phổ biến bao gồm:

  • `required`: Trường này có bắt buộc hay không. Giá trị là boolean hoặc một string (sẽ dùng làm thông báo lỗi).
  • `minLength`: Độ dài tối thiểu của chuỗi. Giá trị là một number, có thể kết hợp với object `{ value: number, message: string }`.
  • `maxLength`: Độ dài tối đa của chuỗi. Tương tự như `minLength`.
  • `min`: Giá trị số tối thiểu (dùng cho input type=”number”). Tương tự như `minLength`.
  • `max`: Giá trị số tối đa. Tương tự như `min`.
  • `pattern`: Áp dụng Regular Expression (Regex) để kiểm tra định dạng. Giá trị là một Regex object, có thể kết hợp với object `{ value: RegExp, message: string }`.
  • `validate`: Một hàm tùy chỉnh để thực hiện validation phức tạp hơn. Hàm này nhận giá trị của input làm đối số và trả về `true` (hợp lệ) hoặc một string (thông báo lỗi) nếu không hợp lệ.

Để hiển thị lỗi, bạn cần lấy object `errors` từ `formState` mà hook `useForm` trả về:


import { useForm } from 'react-hook-form';

function ValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log("Form hợp lệ:", data);
  };

  console.log("Errors:", errors); // Theo dõi object errors khi gõ

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h3>Form có Validation</h3>

      <div>
        <label>Email:</label>
        <input
          type="text"
          {...register("email", {
            required: "Email không được để trống",
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
              message: "Địa chỉ email không hợp lệ"
            }
          })}
        />
        {/* Hiển thị thông báo lỗi */}
        {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
      </div>

      <div>
        <label>Mật khẩu:</label>
        <input
          type="password"
          {...register("password", {
            required: "Mật khẩu không được để trống",
            minLength: {
              value: 6,
              message: "Mật khẩu phải có ít nhất 6 ký tự"
            },
            validate: value =>
              value.includes("!") || value.includes("@") || value.includes("#") || value.includes("$") ||
              "Mật khẩu phải chứa ít nhất một ký tự đặc biệt (!, @, #, $)"
          })}
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
      </div>

      <div>
        <label>Tuổi:</label>
        <input
          type="number"
          {...register("age", {
            min: { value: 18, message: "Bạn phải đủ 18 tuổi" },
            max: { value: 99, message: "Tuổi không hợp lệ" }
          })}
        />
        {errors.age && <p style={{ color: 'red' }}>{errors.age.message}</p>}
      </div>

      <button type="submit">Gửi</button>
    </form>
  );
}

Trong ví dụ này:

  • Chúng ta destructure `formState: { errors }` từ kết quả của `useForm()`.
  • Trong hàm `register` cho mỗi input, chúng ta truyền thêm một object cấu hình validation làm đối số thứ hai.
  • Thông báo lỗi có thể là một string trực tiếp hoặc nằm trong thuộc tính `message` của object cấu hình (`minLength`, `pattern`, `min`, `max`).
  • Chúng ta sử dụng `errors.fieldName` để kiểm tra xem có lỗi cho trường đó không và hiển thị `errors.fieldName.message` nếu có.

React Hook Form sẽ tự động cập nhật object `errors` khi trạng thái validation thay đổi (ví dụ: khi người dùng gõ, hoặc khi blur khỏi input, tùy thuộc vào cấu hình mode validation). Việc hiển thị/ẩn thông báo lỗi trở nên cực kỳ đơn giản.

Làm Việc Với Các Input Phức Tạp (Controller)

Đôi khi, bạn làm việc với các component input từ các thư viện UI bên ngoài (ví dụ: React Select, React Datepicker, Ant Design inputs) mà không cung cấp trực tiếp `ref` hoặc sử dụng `onChange` theo cách truyền thống (`event.target.value`). Trong trường hợp này, hook `register` không đủ. Đây là lúc bạn cần đến `Controller` component mà React Hook Form cung cấp.

`Controller` là một component (không phải hook) giúp “bọc” các controlled inputs (input quản lý state bởi React) và tích hợp chúng vào hệ thống của React Hook Form. Nó kết nối form state với các props mà component bên thứ ba cần (thường là `value` và `onChange`).


import { useForm, Controller } from 'react-hook-form';
import DatePicker from 'react-datepicker'; // Ví dụ dùng react-datepicker
import "react-datepicker/dist/react-datepicker.css"; // Import CSS của Datepicker

function FormWithDatePicker() {
  const { handleSubmit, control } = useForm();

  const onSubmit = (data) => {
    console.log("Form Data:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h3>Form dùng Datepicker (với Controller)</h3>

      <div>
        <label>Ngày sinh:</label>
        <Controller
          name="birthDate" // Tên trường trong form
          control={control} // Kết nối với instance của useForm
          render={({ field }) => (
            // 'field' chứa các props cần thiết: onChange, onBlur, value, ref
            <DatePicker
              placeholderText="Chọn ngày"
              onChange={(date) => field.onChange(date)} // Gửi giá trị về RHF
              selected={field.value} // Nhận giá trị từ RHF để hiển thị
            />
          )}
          rules={{ required: "Vui lòng chọn ngày sinh" }} // Validation rules cho Controller
        />
        {/* Lỗi được truy cập qua formState.errors */}
        {/* {errors.birthDate && <p style={{ color: 'red' }}>{errors.birthDate.message}</p>} */}
        {/* Note: Để errors hoạt động, bạn cần lấy errors từ useForm giống các ví dụ trước */}
        {/* formState: { errors } */}
      </div>

      <button type="submit">Gửi</button>
    </form>
  );
}

Ở đây:

  • Chúng ta lấy `control` từ `useForm()`. `control` là một đối tượng chứa các phương thức và state cần thiết để `Controller` hoạt động.
  • Component `Controller` nhận các props chính: `name` (tên trường), `control` (instance từ `useForm`), `render` (là một render prop – xem lại bài về Render Props).
  • Hàm trong `render` nhận một object chứa `field`, `formState`, `fieldState`. `field` chứa các props quan trọng như `onChange`, `onBlur`, `value`, và `ref` mà bạn sẽ truyền xuống cho component input bên thứ ba.
  • Validation rules cũng có thể được định nghĩa trực tiếp trên `Controller` bằng prop `rules`.

`Controller` giúp RHF làm việc linh hoạt với hầu hết các loại input, kể cả những component phức tạp không tuân theo chuẩn input HTML thông thường.

Các Tính Năng Hữu Ích Khác

React Hook Form còn cung cấp nhiều helper function và state hữu ích khác thông qua object trả về từ `useForm()`:

  • `watch`: Cho phép bạn theo dõi giá trị của một hoặc nhiều input mà không cần component re-render toàn bộ form. Thường dùng để hiển thị giá trị nhập liệu ngay lập tức hoặc thực hiện logic phụ thuộc vào giá trị input (ví dụ: hiển thị mật khẩu đã gõ).
  • `reset`: Đặt lại form về giá trị mặc định (hoặc giá trị bạn chỉ định).
  • `setValue`: Đặt giá trị cho một input cụ thể một cách lập trình.
  • `getValues`: Lấy giá trị hiện tại của toàn bộ form hoặc một input cụ thể bất kỳ lúc nào.
  • `setFocus`: Đặt focus vào một input cụ thể.
  • `formState`: Chứa các thông tin về trạng thái hiện tại của form như `errors`, `isSubmitting`, `isDirty`, `isValid`, v.v.

Việc nắm vững các helper này giúp bạn xây dựng các form có tính năng phong phú và trải nghiệm người dùng tốt hơn.

So Sánh Ngắn: RHF vs Formik vs useState Thủ Công

Để củng cố thêm lý do tại sao RHF là lựa chọn tuyệt vời, hãy cùng xem bảng so sánh ngắn với các phương pháp khác:

Tính năng React Hook Form Formik Xử lý thủ công (useState)
Cách quản lý Input Uncontrolled (dùng Refs), kết nối qua register. Controlled (quản lý state nội bộ). Controlled (quản lý state bằng useState trong component).
Hiệu năng (Số lần re-render) Rất tốt (Ít re-render component chứa form). Tốt (Component form re-render khi state Formik thay đổi). Kém (Component re-render trên mỗi lần gõ phím).
Đơn giản API Đơn giản, trực quan với Hooks (useForm, register, handleSubmit). API dựa trên Render Props hoặc Hook (useFormik), cần cấu hình nhiều hơn ban đầu. Viết thủ công cho từng input (useState, hàm onChange).
Kích thước Bundle Nhỏ. Lớn hơn RHF. Không thư viện, nhưng lượng code tự viết có thể lớn.
Validation Built-in mạnh mẽ, hỗ trợ schema (Yup, Zod…). Thường dùng kèm Yup cho schema validation. Viết thủ công hoặc dùng thư viện validation riêng.
Learning Curve Dễ bắt đầu với các form đơn giản, mạnh mẽ khi đi sâu. Cần làm quen với cấu trúc và các khái niệm riêng của Formik. Dễ hiểu ban đầu cho form nhỏ, phức tạp nhanh chóng với form lớn.

Bảng trên cho thấy RHF có lợi thế đáng kể về hiệu năng và đơn giản hóa code cho hầu hết các trường hợp.

Best Practices & Mẹo Nhỏ

  • Tích hợp Schema Validation (Yup, Zod): Đối với các form phức tạp với nhiều ràng buộc validation, việc tích hợp với các thư viện schema như Yup hoặc Zod là rất nên làm. RHF hỗ trợ điều này rất tốt thông qua tùy chọn `resolver` trong `useForm`, giúp quản lý validation rules tập trung và dễ đọc hơn.
  • Chia nhỏ Component: Nếu form của bạn quá lớn, hãy chia nó thành các component con. Truyền các phương thức của RHF (như `register`, `control`, `formState`) xuống các component con thông qua props. Điều này giúp code dễ quản lý hơn và có thể cải thiện hiệu năng nếu các component con không re-render khi form state chung thay đổi (do RHF quản lý state ở cấp độ hook, không phải component).
  • Accessibility (Tiếp cận): Đừng quên các thuộc tính accessibility cho form của bạn, đặc biệt là liên kết label với input bằng `htmlFor` và `id`, và sử dụng các thuộc tính ARIA để thông báo trạng thái validation cho người dùng sử dụng trình đọc màn hình. RHF không tự động làm điều này hoàn toàn, bạn cần kết hợp thủ công.
  • Xử lý trạng thái loading/error khi submit: Khi gửi form đi (ví dụ gọi API – xem lại bài về Fetch/Axiosbài về React Query/SWR), bạn nên sử dụng state trong component hoặc từ các thư viện quản lý state (Context, Redux Toolkit, Recoil/Zustand) để hiển thị trạng thái loading cho nút submit hoặc thông báo lỗi từ server.

Kết Luận

Xử lý form trong React không còn là một công việc nhàm chán và phức tạp nếu bạn sử dụng đúng công cụ. React Hook Form với API dựa trên hooks, hiệu năng vượt trội nhờ uncontrolled inputs, và khả năng validation linh hoạt đã chứng minh nó là một trong những thư viện quản lý form hàng đầu hiện nay.

Việc chuyển từ quản lý state form thủ công sang sử dụng React Hook Form sẽ giúp code của bạn gọn gàng hơn, dễ bảo trì hơn, và mang lại trải nghiệm tốt hơn cho cả nhà phát triển lẫn người dùng cuối.

Đây là một bước tiến quan trọng trong hành trình làm chủ React của bạn. Nắm vững cách xử lý form hiệu quả là kỹ năng không thể thiếu đối với bất kỳ React Developer nào.

Cảm ơn các bạn đã theo dõi bài viết trong series “React Roadmap”. Hy vọng nội dung này hữu ích cho con đường học React của bạn. Hãy thực hành thật nhiều để làm quen với React Hook Form nhé! Hẹn gặp lại trong các bài viết tiếp theo, nơi chúng ta sẽ tiếp tục khám phá những khía cạnh thú vị khác của React.

Happy Coding!

Chỉ mục