[React Basics] Xử lý Form trong React: Hiểu rõ State, Controlled Components và Validation

Chào bạn, dù bạn đang xây dựng một form đăng nhập đơn giản hay một trang cấu hình sản phẩm phức tạp, việc xử lý form là một kỹ năng không thể thiếu đối với bất kỳ lập trình viên React nào. Form chính là cây cầu nối quan trọng nhất giữa người dùng và ứng dụng của bạn.

Xử lý Form trong React

Tuy nhiên, cách React xử lý form có một chút khác biệt so với HTML truyền thống. Thay vì để DOM tự quản lý trạng thái, React khuyến khích chúng ta sử dụng state của component làm "nguồn chân lý duy nhất" (single source of truth).

Trong bài viết này, chúng ta sẽ cùng nhau đi sâu vào mọi ngóc ngách của việc xử lý form trong React, từ những khái niệm nền tảng nhất đến các thư viện chuyên nghiệp giúp bạn làm việc hiệu quả hơn.

1. Nền tảng cốt lõi: Controlled vs. Uncontrolled Components

Trong thế giới React, có hai triết lý chính để quản lý dữ liệu trong form. Hiểu rõ sự khác biệt giữa chúng là chìa khóa để viết code hiệu quả và dễ bảo trì.

Controlled vs. Uncontrolled Components

Controlled Components 💡

Đây là cách tiếp cận được khuyến khích và phổ biến nhất trong React.

  • Ý tưởng: Trạng thái của các thẻ input (như <input>, <textarea>, <select>) được "kiểm soát" hoàn toàn bởi state của React. Mọi thay đổi của người dùng sẽ cập nhật state, và state sẽ quyết định giá trị hiển thị trên giao diện.

  • Cách hoạt động:

    1. Sử dụng useState để lưu trữ giá trị của input.
    2. Gán giá trị này vào prop value của thẻ input.
    3. Tạo một hàm xử lý sự kiện onChange để cập nhật state mỗi khi người dùng gõ phím.
  • Ví dụ kinh điển: Một form đăng nhập đơn giản

    import React, { useState } from 'react'
    
    function LoginForm() {
      // 1. Dùng state để lưu trữ giá trị
      const [username, setUsername] = useState('')
    
      const handleChange = (event) => {
        // 3. Cập nhật state mỗi khi có thay đổi
        setUsername(event.target.value)
      }
    
      const handleSubmit = (event) => {
        event.preventDefault() // Ngăn trình duyệt reload
        alert(`Tên đăng nhập của bạn là: ${username}`)
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <label>
            Tên đăng nhập:
            {/* 2. Gán state vào prop 'value' */}
            <input type="text" value={username} onChange={handleChange} />
          </label>
          <button type="submit">Gửi</button>
        </form>
      )
    }
    
  • Ưu điểm:

    • Nguồn chân lý duy nhất: Dữ liệu form và state của component luôn đồng bộ.
    • Dễ dàng validation: Bạn có thể kiểm tra và hiển thị lỗi ngay lập tức trong hàm onChange.
    • Khả năng kiểm soát cao: Có thể định dạng dữ liệu (ví dụ: chỉ cho phép nhập số) hoặc vô hiệu hóa nút submit một cách linh hoạt.

Uncontrolled Components

  • Ý tưởng: Trái ngược với Controlled Components, ở đây chúng ta để cho DOM tự quản lý trạng thái của input. React chỉ "hỏi" DOM về giá trị của input khi cần thiết (thường là khi submit form).

  • Cách hoạt động: Sử dụng useRef để tạo một tham chiếu trực tiếp đến phần tử DOM của input. Khi cần lấy giá trị, bạn sẽ truy cập qua ref.current.value.

  • Ví dụ:

    import React, { useRef } from 'react'
    
    function UncontrolledForm() {
      // 1. Dùng useRef để tham chiếu đến DOM
      const inputRef = useRef(null)
    
      const handleSubmit = (event) => {
        event.preventDefault()
        // 2. Lấy giá trị trực tiếp từ DOM khi submit
        alert(`Giá trị nhập vào là: ${inputRef.current.value}`)
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <label>
            Tên của bạn:
            <input type="text" ref={inputRef} />
          </label>
          <button type="submit">Gửi</button>
        </form>
      )
    }
    
  • Khi nào nên dùng?

    • Các form cực kỳ đơn giản không cần validation tức thời.
    • Tích hợp với các thư viện không phải React.
    • Khi hiệu năng là ưu tiên hàng đầu và bạn muốn tránh re-render mỗi khi gõ phím (mặc dù điều này hiếm khi là vấn đề).

Lời khuyên: Hãy luôn ưu tiên sử dụng Controlled Components. Nó giúp code của bạn dễ đoán, dễ quản lý và phù hợp với triết lý của React hơn.

2. Xử lý các tình huống phổ biến của Form

Khi form trở nên phức tạp hơn, bạn sẽ cần các kỹ thuật sau:

Quản lý nhiều input

Thay vì tạo một state cho mỗi input, bạn có thể dùng một state object duy nhất và một hàm onChange chung.

function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
  })

  const handleChange = (event) => {
    const { name, value } = event.target
    setFormData((prevFormData) => ({
      ...prevFormData,
      [name]: value, // Dùng computed property name để cập nhật đúng field
    }))
  }

  // ... handleSubmit ...

  return (
    <form>
      <input
        type="text"
        name="username" // Thuộc tính 'name' rất quan trọng!
        value={formData.username}
        onChange={handleChange}
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
      />
    </form>
  )
}

Các loại input khác

Ngoài loại input đơn giản, chúng ta cũng thường xuyên cần sử dụng tới các loại input khác.

  • Textarea: Tương tự như <input type="text">, sử dụng prop value.
    <textarea value={value} onChange={handleChange} />
    
  • Select (Dropdown): Gán value vào thẻ <select> thay vì dùng thuộc tính selected trên <option>.
    <select value={selectedValue} onChange={handleChange}>
      <option value="volvo">Volvo</option>
      <option value="saab">Saab</option>
    </select>
    
  • Checkbox và Radio: Sử dụng prop checked thay vì value để kiểm soát trạng thái.
    <input
      type="checkbox"
      name="isFriendly"
      checked={isChecked}
      onChange={handleCheckboxChange}
    />
    

3. Form Validation: Xử lý các trường hợp đơn giản

Validation là "linh hồn" của một form hoạt động tốt. Bạn có thể thực hiện validation thủ công ngay trong component.

  • Validation đơn giản:

    function PasswordForm() {
      const [password, setPassword] = useState('')
      const [error, setError] = useState('')
    
      const handlePasswordChange = (e) => {
        const newPassword = e.target.value
        setPassword(newPassword)
        if (newPassword.length < 8) {
          setError('Mật khẩu phải có ít nhất 8 ký tự.')
        } else {
          setError('')
        }
      }
    
      return (
        <>
          <input
            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
          {error && <p style={{ color: 'red' }}>{error}</p>}
        </>
      )
    }
    

Tuy nhiên, khi logic validation trở nên phức tạp, việc quản lý thủ công sẽ rất cồng kềnh. Đó là lúc chúng ta cần tới các thư viện để trợ giúp làm điều này.

4. Các thư viện quản lý Form chuyên nghiệp 🛠️

Việc viết đi viết lại logic quản lý state, change, validation... rất tốn thời gian. Các thư viện ra đời để giải quyết vấn đề này, giúp bạn tập trung vào giao diện và logic nghiệp vụ.

Hai thư viện phổ biến và mạnh mẽ nhất hiện nay là React Hook FormFormik.

React Hook Form

Nổi lên như một thế lực mới, được yêu thích vì hiệu năng cao và cú pháp gọn gàng, tận dụng tối đa sức mạnh của Hooks.

React Hook Form

  • Điểm mạnh:

    • Hiệu năng vượt trội: Giảm thiểu số lần re-render không cần thiết bằng cách sử dụng ref (giống Uncontrolled) nhưng vẫn cung cấp API mạnh mẽ để bạn làm việc (giống Controlled).
    • Dễ sử dụng: Cú pháp hook trực quan, dễ học.
    • Tích hợp dễ dàng: Hoạt động mượt mà với các thư viện validation schema như Yup hoặc Zod.
  • Ví dụ với React Hook Form + Yup:

    import { useForm } from 'react-hook-form'
    import { yupResolver } from '@hookform/resolvers/yup'
    import * as yup from 'yup'
    
    // 1. Định nghĩa schema validation
    const schema = yup
      .object()
      .shape({
        email: yup
          .string()
          .email('Email không hợp lệ')
          .required('Vui lòng nhập email'),
        password: yup
          .string()
          .min(8, 'Mật khẩu quá ngắn')
          .required('Vui lòng nhập mật khẩu'),
      })
      .required()
    
    function MyForm() {
      const {
        register,
        handleSubmit,
        formState: { errors },
      } = useForm({
        resolver: yupResolver(schema), // 2. Tích hợp resolver
      })
    
      // 4. Hàm xử lý khi form hợp lệ
      const onSubmit = (data) => console.log(data)
    
      return (
        // 3. Gắn handleSubmit
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register('email')} />
          <p>{errors.email?.message}</p> {/* Hiển thị lỗi */}
          <input type="password" {...register('password')} />
          <p>{errors.password?.message}</p>
          <input type="submit" />
        </form>
      )
    }
    

    Bạn có thấy code gọn gàng hơn rất nhiều không? Không còn useState, không còn onChange thủ công!

Formik

Là một thư viện kỳ cựu, rất ổn định và có một cộng đồng lớn. Formik cung cấp một bộ công cụ hoàn chỉnh để quản lý trạng thái, xử lý sự kiện và validation.

Formik

  • Điểm mạnh:
    • Toàn diện: Cung cấp giải pháp "tất cả trong một".
    • Kinh nghiệm và ổn định: Đã được kiểm chứng qua nhiều dự án lớn.
    • Tài liệu tốt.

Kết luận: Xử lý Form trong React không khó

Hy vọng bài viết này đã cung cấp cho bạn một cái nhìn toàn cảnh và sâu sắc về cách xử lý form trong React. Giờ đây, bạn đã được trang bị đầy đủ kiến thức để xây dựng những chiếc form mạnh mẽ, linh hoạt và thân thiện với người dùng.

Dưới đây là phần tổng hợp nhanh nội dung:

  • Bắt đầu với Controlled Components: Đây là kiến thức nền tảng bạn phải nắm vững.
  • Khi form phức tạp: Đừng ngần ngại sử dụng một thư viện. Nó sẽ tiết kiệm cho bạn rất nhiều thời gian và công sức.
  • Nên chọn thư viện nào?
    • React Hook Form đang là lựa chọn hàng đầu cho các dự án mới vì hiệu năng và cú pháp hiện đại.
    • Formik vẫn là một lựa chọn tuyệt vời, đặc biệt nếu bạn đã quen thuộc với nó hoặc cần một giải pháp đã được kiểm chứng qua thời gian.
  • Đừng quên trải nghiệm người dùng: Luôn cung cấp phản hồi rõ ràng (thông báo lỗi, trạng thái loading...) để giúp người dùng tương tác với form của bạn một cách dễ dàng nhất.

Chúc bạn thành công trên con đường chinh phục React!

Bài viết liên quan

[React Basics] Thuộc tính key trong React: Hiểu rõ và sử dụng hiệu quả

Bạn đã thực sự hiểu về thuộc tính key trong React? Tìm hiểu vai trò, cách dùng hiệu quả và các ví dụ thực tế giúp code của bạn sạch và tối ưu hơn.

[React Basics] Props trong React: Khái niệm, cách dùng và ví dụ thực tế

Giải mã props trong ReactJS: từ khái niệm cơ bản đến cách truyền dữ liệu giữa các component một cách hiệu quả. Khám phá các ví dụ minh họa dễ hiểu để bạn áp dụng ngay.

[React Basics] State trong React: Khái niệm, cách sử dụng và ví dụ chi tiết

Tìm hiểu khái niệm, cách khai báo và sử dụng State để quản lý dữ liệu động trong ứng dụng React của bạn. Xem ngay hướng dẫn chi tiết kèm ví dụ.

[React Basics] JSX trong React là gì? Hướng dẫn chi tiết cho người mới bắt đầu

JSX là cú pháp mở rộng của JavaScript, cho phép viết code giống HTML trong React. Bài viết này sẽ giúp bạn hiểu rõ JSX là gì, tại sao cần dùng JSX và cách sử dụng hiệu quả.