Tiếng Việt

Khai phá sức mạnh của hook useActionState trong React. Tìm hiểu cách nó đơn giản hóa việc quản lý form, xử lý các trạng thái chờ và nâng cao trải nghiệm người dùng với các ví dụ thực tế, chuyên sâu.

React useActionState: Hướng Dẫn Toàn Diện về Quản Lý Form Hiện Đại

Thế giới phát triển web không ngừng biến đổi, và hệ sinh thái React luôn đi đầu trong sự thay đổi này. Với các phiên bản gần đây, React đã giới thiệu những tính năng mạnh mẽ giúp cải thiện cơ bản cách chúng ta xây dựng các ứng dụng tương tác và linh hoạt. Một trong những tính năng có ảnh hưởng lớn nhất là hook useActionState, một yếu tố thay đổi cuộc chơi trong việc xử lý form và các hoạt động bất đồng bộ. Hook này, trước đây được biết đến với tên gọi useFormState trong các bản phát hành thử nghiệm, giờ đây đã trở thành một công cụ ổn định và cần thiết cho bất kỳ nhà phát triển React hiện đại nào.

Hướng dẫn toàn diện này sẽ đưa bạn đi sâu vào useActionState. Chúng ta sẽ khám phá những vấn đề mà nó giải quyết, cơ chế cốt lõi của nó, và cách tận dụng nó cùng với các hook bổ sung như useFormStatus để tạo ra những trải nghiệm người dùng vượt trội. Dù bạn đang xây dựng một form liên hệ đơn giản hay một ứng dụng phức tạp, nhiều dữ liệu, việc hiểu rõ useActionState sẽ giúp mã của bạn sạch sẽ hơn, có tính khai báo cao hơn và mạnh mẽ hơn.

Vấn đề: Sự Phức Tạp của Việc Quản Lý Trạng Thái Form Truyền Thống

Trước khi chúng ta có thể đánh giá cao sự tinh tế của useActionState, chúng ta phải hiểu những thách thức mà nó giải quyết. Trong nhiều năm, việc quản lý trạng thái form trong React liên quan đến một khuôn mẫu có thể đoán trước nhưng thường cồng kềnh khi sử dụng hook useState.

Hãy xem xét một kịch bản phổ biến: một form đơn giản để thêm sản phẩm mới vào danh sách. Chúng ta cần quản lý một số mẩu trạng thái:

Một cách triển khai điển hình có thể trông như thế này:

Ví dụ: 'Cách cũ' với nhiều hook useState

// Hàm API giả định
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Tên sản phẩm phải có ít nhất 3 ký tự.');
}
console.log(`Đã thêm sản phẩm "${productName}".`);
return { success: true };
};

// Component
import { useState } from 'react';

function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);

const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);

try {
await addProductAPI(productName);
setProductName(''); // Xóa ô nhập liệu khi thành công
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};

return (




id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>

{error &&

{error}

}


);
}

Cách tiếp cận này hoạt động, nhưng nó có một số nhược điểm:

  • Mã lặp lại (Boilerplate): Chúng ta cần ba lệnh gọi useState riêng biệt để quản lý một quá trình gửi form về mặt khái niệm là duy nhất.
  • Quản lý trạng thái thủ công: Nhà phát triển chịu trách nhiệm thiết lập và đặt lại thủ công các trạng thái đang tải và lỗi theo đúng thứ tự trong một khối try...catch...finally. Điều này lặp đi lặp lại và dễ gây ra lỗi.
  • 耦合 (Coupling): Logic xử lý kết quả gửi form bị ràng buộc chặt chẽ với logic render của component.

Giới thiệu useActionState: Một sự thay đổi trong tư duy

useActionState là một hook của React được thiết kế đặc biệt để quản lý trạng thái của một hành động bất đồng bộ, chẳng hạn như gửi form. Nó hợp lý hóa toàn bộ quá trình bằng cách kết nối trực tiếp trạng thái với kết quả của hàm hành động.

Chữ ký của nó rõ ràng và ngắn gọn:

const [state, formAction] = useActionState(actionFn, initialState);

Hãy phân tích các thành phần của nó:

  • actionFn(previousState, formData): Đây là hàm bất đồng bộ của bạn thực hiện công việc (ví dụ: gọi API). Nó nhận trạng thái trước đó và dữ liệu form làm đối số. Quan trọng là, bất cứ điều gì hàm này trả về sẽ trở thành trạng thái mới.
  • initialState: Đây là giá trị của trạng thái trước khi hành động được thực thi lần đầu tiên.
  • state: Đây là trạng thái hiện tại. Ban đầu nó giữ initialState và được cập nhật thành giá trị trả về của actionFn sau mỗi lần thực thi.
  • formAction: Đây là một phiên bản mới, được bọc lại của hàm hành động của bạn. Bạn nên truyền hàm này vào prop action của phần tử <form>. React sử dụng hàm được bọc này để theo dõi trạng thái chờ của hành động.

Ví dụ thực tế: Tái cấu trúc với useActionState

Bây giờ, hãy tái cấu trúc form sản phẩm của chúng ta bằng useActionState. Sự cải thiện có thể thấy ngay lập tức.

Đầu tiên, chúng ta cần điều chỉnh logic hành động của mình. Thay vì ném lỗi, hành động nên trả về một đối tượng trạng thái mô tả kết quả.

Ví dụ: 'Cách mới' với useActionState

// Hàm hành động, được thiết kế để hoạt động với useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // Giả lập độ trễ mạng

if (!productName || productName.length < 3) {
return { message: 'Tên sản phẩm phải có ít nhất 3 ký tự.', success: false };
}

console.log(`Đã thêm sản phẩm "${productName}".`);
// Khi thành công, trả về thông báo thành công và xóa form.
return { message: `Đã thêm thành công "${productName}"`, success: true };
};

// Component đã được tái cấu trúc
import { useActionState } from 'react';
// Lưu ý: Chúng ta sẽ thêm useFormStatus trong phần tiếp theo để xử lý trạng thái chờ.

function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);

return (





{!state.success && state.message && (

{state.message}


)}
{state.success && state.message && (

{state.message}


)}

);
}

Hãy xem nó sạch sẽ hơn bao nhiêu! Chúng ta đã thay thế ba hook useState bằng một hook useActionState duy nhất. Trách nhiệm của component bây giờ hoàn toàn là render UI dựa trên đối tượng `state`. Tất cả logic nghiệp vụ được gói gọn gàng trong hàm `addProductAction`. Trạng thái tự động cập nhật dựa trên những gì hành động trả về.

Nhưng khoan đã, còn trạng thái chờ thì sao? Làm thế nào để vô hiệu hóa nút trong khi form đang gửi đi?

Xử lý Trạng thái Chờ với useFormStatus

React cung cấp một hook đồng hành, useFormStatus, được thiết kế để giải quyết chính xác vấn đề này. Nó cung cấp thông tin trạng thái cho lần gửi form cuối cùng, nhưng với một quy tắc quan trọng: nó phải được gọi từ một component được render bên trong <form> mà bạn muốn theo dõi trạng thái.

Điều này khuyến khích sự tách biệt rõ ràng các mối quan tâm. Bạn tạo một component đặc biệt cho các yếu tố UI cần nhận biết về trạng thái gửi của form, như một nút submit.

Hook useFormStatus trả về một đối tượng với một số thuộc tính, trong đó quan trọng nhất là `pending`.

const { pending, data, method, action } = useFormStatus();

  • pending: Một giá trị boolean là `true` nếu form cha hiện đang gửi và `false` nếu không.
  • data: Một đối tượng `FormData` chứa dữ liệu đang được gửi.
  • method: Một chuỗi cho biết phương thức HTTP (`'get'` hoặc `'post'`).
  • action: Một tham chiếu đến hàm được truyền cho prop `action` của form.

Tạo một Nút Submit Nhận biết Trạng thái

Hãy tạo một component `SubmitButton` chuyên dụng và tích hợp nó vào form của chúng ta.

Ví dụ: Component SubmitButton

import { useFormStatus } from 'react-dom';
// Lưu ý: useFormStatus được import từ 'react-dom', không phải 'react'.

function SubmitButton() {
const { pending } = useFormStatus();

return (

);
}

Bây giờ, chúng ta có thể cập nhật component form chính của mình để sử dụng nó.

Ví dụ: Form hoàn chỉnh với useActionState và useFormStatus

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

// ... (hàm addProductAction vẫn giữ nguyên)

function SubmitButton() { /* ... như định nghĩa ở trên ... */ }

function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);

return (



{/* Chúng ta có thể thêm một key để reset input khi thành công */}


{!state.success && state.message && (

{state.message}


)}
{state.success && state.message && (

{state.message}


)}

);
}

Với cấu trúc này, component `CompleteProductForm` không cần biết gì về trạng thái chờ. `SubmitButton` hoàn toàn tự chứa. Mẫu hình compositional này cực kỳ mạnh mẽ để xây dựng các UI phức tạp, dễ bảo trì.

Sức mạnh của Cải tiến Lũy tiến (Progressive Enhancement)

Một trong những lợi ích sâu sắc nhất của cách tiếp cận dựa trên hành động mới này, đặc biệt khi được sử dụng với Server Actions, là cải tiến lũy tiến tự động. Đây là một khái niệm quan trọng để xây dựng các ứng dụng cho khán giả toàn cầu, nơi điều kiện mạng có thể không đáng tin cậy và người dùng có thể có các thiết bị cũ hơn hoặc đã tắt JavaScript.

Đây là cách nó hoạt động:

  1. Không có JavaScript: Nếu trình duyệt của người dùng không thực thi JavaScript phía client, `<form action={...}>` hoạt động như một form HTML tiêu chuẩn. Nó thực hiện một yêu cầu toàn trang đến máy chủ. Nếu bạn đang sử dụng một framework như Next.js, hành động phía máy chủ sẽ chạy và framework sẽ render lại toàn bộ trang với trạng thái mới (ví dụ: hiển thị lỗi xác thực). Ứng dụng hoàn toàn hoạt động, chỉ là không có sự mượt mà như SPA.
  2. Với JavaScript: Khi gói JavaScript tải xong và React hydrate trang, cùng một `formAction` sẽ được thực thi phía client. Thay vì tải lại toàn bộ trang, nó hoạt động giống như một yêu cầu fetch thông thường. Hành động được gọi, trạng thái được cập nhật, và chỉ những phần cần thiết của component mới được render lại.

Điều này có nghĩa là bạn viết logic form của mình một lần và nó hoạt động liền mạch trong cả hai kịch bản. Bạn xây dựng một ứng dụng linh hoạt, dễ tiếp cận theo mặc định, đây là một chiến thắng lớn cho trải nghiệm người dùng trên toàn cầu.

Các Mẫu Nâng cao và Trường hợp Sử dụng

1. Server Actions vs. Client Actions

Hàm `actionFn` bạn truyền cho useActionState có thể là một hàm async phía client tiêu chuẩn (như trong ví dụ của chúng ta) hoặc một Server Action. Một Server Action là một hàm được định nghĩa trên máy chủ có thể được gọi trực tiếp từ các component phía client. Trong các framework như Next.js, bạn định nghĩa một hàm như vậy bằng cách thêm chỉ thị "use server"; ở đầu thân hàm.

  • Client Actions: Lý tưởng cho các thay đổi chỉ ảnh hưởng đến trạng thái phía client hoặc gọi các API của bên thứ ba trực tiếp từ client.
  • Server Actions: Hoàn hảo cho các thay đổi liên quan đến cơ sở dữ liệu hoặc các tài nguyên phía máy chủ khác. Chúng đơn giản hóa kiến trúc của bạn bằng cách loại bỏ nhu cầu tạo các điểm cuối API thủ công cho mỗi thay đổi.

Vẻ đẹp của nó là useActionState hoạt động giống hệt với cả hai. Bạn có thể hoán đổi một client action cho một server action mà không cần thay đổi mã component.

2. Cập nhật lạc quan (Optimistic Updates) với `useOptimistic`

Để có cảm giác phản hồi nhanh hơn nữa, bạn có thể kết hợp useActionState với hook useOptimistic. Một cập nhật lạc quan là khi bạn cập nhật UI ngay lập tức, *giả sử* hành động bất đồng bộ sẽ thành công. Nếu nó thất bại, bạn sẽ hoàn nguyên UI về trạng thái trước đó.

Hãy tưởng tượng một ứng dụng mạng xã hội nơi bạn thêm một bình luận. Một cách lạc quan, bạn sẽ hiển thị bình luận mới trong danh sách ngay lập tức trong khi yêu cầu đang được gửi đến máy chủ. useOptimistic được thiết kế để hoạt động song song với các hành động để làm cho mẫu này trở nên dễ thực hiện.

3. Reset một Form khi Thành công

Một yêu cầu phổ biến là xóa các trường nhập liệu của form sau khi gửi thành công. Có một vài cách để đạt được điều này với useActionState.

  • Mẹo dùng Prop `key`: Như được trình bày trong ví dụ `CompleteProductForm` của chúng ta, bạn có thể gán một `key` duy nhất cho một input hoặc toàn bộ form. Khi key thay đổi, React sẽ gỡ bỏ component cũ và gắn một component mới, thực chất là reset trạng thái của nó. Gắn key với một cờ báo thành công (`key={state.success ? 'success' : 'initial'}`) là một phương pháp đơn giản và hiệu quả.
  • Component có kiểm soát (Controlled Components): Bạn vẫn có thể sử dụng các component có kiểm soát nếu cần. Bằng cách quản lý giá trị của input với useState, bạn có thể gọi hàm setter để xóa nó bên trong một useEffect lắng nghe trạng thái thành công từ useActionState.

Những Cạm bẫy Phổ biến và Thực tiễn Tốt nhất

  • Vị trí của useFormStatus: Hãy nhớ rằng, một component gọi useFormStatus phải được render như một con của <form>. Nó sẽ không hoạt động nếu nó là anh em hoặc cha mẹ.
  • Trạng thái có thể tuần tự hóa (Serializable State): Khi sử dụng Server Actions, đối tượng trạng thái được trả về từ hành động của bạn phải có thể tuần tự hóa. Điều này có nghĩa là nó không thể chứa các hàm, Symbols, hoặc các giá trị không thể tuần tự hóa khác. Hãy sử dụng các đối tượng đơn giản, mảng, chuỗi, số và boolean.
  • Đừng ném lỗi trong Actions: Thay vì `throw new Error()`, hàm hành động của bạn nên xử lý lỗi một cách duyên dáng và trả về một đối tượng trạng thái mô tả lỗi (ví dụ: `{ success: false, message: 'Đã xảy ra lỗi' }`). Điều này đảm bảo trạng thái luôn được cập nhật một cách có thể dự đoán được.
  • Định nghĩa một Cấu trúc Trạng thái Rõ ràng: Thiết lập một cấu trúc nhất quán cho đối tượng trạng thái của bạn ngay từ đầu. Một cấu trúc như `{ data: T | null, message: string | null, success: boolean, errors: Record | null }` có thể bao gồm nhiều trường hợp sử dụng.

useActionState vs. useReducer: So sánh nhanh

Thoạt nhìn, useActionState có vẻ tương tự như useReducer, vì cả hai đều liên quan đến việc cập nhật trạng thái dựa trên trạng thái trước đó. Tuy nhiên, chúng phục vụ các mục đích khác nhau.

  • useReducer là một hook đa năng để quản lý các chuyển đổi trạng thái phức tạp ở phía client. Nó được kích hoạt bằng cách dispatch các hành động và lý tưởng cho logic trạng thái có nhiều thay đổi trạng thái đồng bộ có thể xảy ra (ví dụ: một trình hướng dẫn đa bước phức tạp).
  • useActionState là một hook chuyên dụng được thiết kế cho trạng thái thay đổi để đáp ứng một hành động bất đồng bộ duy nhất, thường là. Vai trò chính của nó là tích hợp với các form HTML, Server Actions, và các tính năng render đồng thời của React như các chuyển đổi trạng thái chờ.

Kết luận: Đối với việc gửi form và các hoạt động bất đồng bộ gắn liền với form, useActionState là công cụ hiện đại, được xây dựng có mục đích. Đối với các máy trạng thái phức tạp khác ở phía client, useReducer vẫn là một lựa chọn tuyệt vời.

Kết luận: Đón nhận Tương lai của Form trong React

Hook useActionState không chỉ là một API mới; nó đại diện cho một sự thay đổi cơ bản hướng tới một cách xử lý form và các thay đổi dữ liệu trong React mạnh mẽ hơn, có tính khai báo cao hơn và lấy người dùng làm trung tâm. Bằng cách áp dụng nó, bạn sẽ có được:

  • Giảm mã lặp lại: Một hook duy nhất thay thế nhiều lệnh gọi useState và việc điều phối trạng thái thủ công.
  • Tích hợp Trạng thái Chờ: Xử lý liền mạch các UI đang tải với hook đồng hành useFormStatus.
  • Tích hợp sẵn Cải tiến Lũy tiến: Viết mã hoạt động có hoặc không có JavaScript, đảm bảo khả năng tiếp cận và sự linh hoạt cho tất cả người dùng.
  • Đơn giản hóa Giao tiếp với Máy chủ: Phù hợp tự nhiên với Server Actions, hợp lý hóa trải nghiệm phát triển full-stack.

Khi bạn bắt đầu các dự án mới hoặc tái cấu trúc các dự án hiện có, hãy cân nhắc sử dụng useActionState. Nó không chỉ cải thiện trải nghiệm của nhà phát triển bằng cách làm cho mã của bạn sạch hơn và dễ dự đoán hơn mà còn trao quyền cho bạn để xây dựng các ứng dụng chất lượng cao hơn, nhanh hơn, linh hoạt hơn và dễ tiếp cận với một lượng khán giả toàn cầu đa dạng.