React 19 Actions trong thực tế: useActionState, useOptimistic & khả năng phục hồi của Server Action

Mục lục bài viết(17 mục)
React 19 mang đến một sự thay đổi mô hình trong việc xử lý các thay đổi dữ liệu và tương tác với máy chủ, chủ yếu thông qua Actions. Hướng dẫn này sẽ phân tích useActionState, useOptimistic và useFormStatus trong ngữ cảnh Next.js 15 App Router, trình bày cách áp dụng chúng để xây dựng các biểu mẫu mạnh mẽ, lấy người dùng làm trung tâm. Chúng ta sẽ đề cập đến cập nhật lạc quan (optimistic updates), xử lý lỗi, quy trình làm việc nhiều bước và xác thực schema mạnh mẽ.
Lộ trình Kiến trúc Next.js 15 & React 19
React Actions: Mô hình thay đổi dữ liệu hợp nhất
React Actions cung cấp một cơ chế tiêu chuẩn để các component tương tác với các thay đổi dữ liệu phía máy chủ. Chúng trừu tượng hóa sự phức tạp của các yêu cầu mạng, xác thực lại và quản lý trạng thái, cho phép các nhà phát triển tập trung vào logic nghiệp vụ. Actions có thể được gọi trực tiếp từ các phần tử <form> hoặc theo chương trình thông qua startTransition.
Các Hook cốt lõi
useActionState: Quản lý trạng thái và giá trị trả về của một server action. Nó cung cấp kết quả cuối cùng của action và một hàm dispatch để gọi nó. Quan trọng là, nó xử lý các trạng thái đang chờ xử lý và re-render khi action hoàn thành.useOptimistic: Cho phép cập nhật UI ngay lập tức để dự đoán một server action thành công. Nó cung cấp một trạng thái "lạc quan" tạm thời có thể được hoàn tác nếu action thất bại, hoặc xác nhận nếu nó thành công.useFormStatus: Một hook phía client cung cấp thông tin về trạng thái đang chờ xử lý của việc gửi<form>cha. Hữu ích cho việc vô hiệu hóa các nút hoặc hiển thị các chỉ báo tải.
Tổng quan kiến trúc: Server Components & Actions
Next.js 15 App Router tận dụng React Server Components (RSCs) và Server Actions để làm mờ ranh giới giữa client và server. Actions là các hàm được đánh dấu bằng 'use server' chỉ thực thi trên máy chủ. Chúng có thể được định nghĩa trực tiếp trong Server Components hoặc trong các tệp riêng biệt.
Luồng dữ liệu với Actions
- Tương tác Client: Người dùng gửi biểu mẫu hoặc kích hoạt một action.
- Gọi Action: Hàm action được gọi.
- Thực thi Server: Server action thực thi, thực hiện các thao tác cơ sở dữ liệu và trả về kết quả.
- Cập nhật Client: React re-render các component dựa trên kết quả của action, có thể xác thực lại các bộ nhớ đệm dữ liệu.
Triển khai một biểu mẫu cấp độ sản xuất
Hãy xem xét một biểu mẫu cập nhật hồ sơ người dùng yêu cầu xác thực, cập nhật lạc quan và tiến trình nhiều bước.
Định nghĩa Schema với Zod
Xác thực mạnh mẽ là rất quan trọng. Chúng ta sẽ sử dụng Zod cho cả định nghĩa schema phía client và server.
// lib/schemas.ts
import { z } from 'zod';
export const profileSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters.').max(50, 'Username cannot exceed 50 characters.'),
email: z.string().email('Invalid email address.'),
bio: z.string().max(200, 'Bio cannot exceed 200 characters.').optional(),
});
export const passwordSchema = z.object({
currentPassword: z.string().min(8, 'Current password must be at least 8 characters.'),
newPassword: z.string().min(8, 'New password must be at least 8 characters.'),
confirmNewPassword: z.string(),
}).refine((data) => data.newPassword === data.confirmNewPassword, {
message: 'New passwords do not match.',
path: ['confirmNewPassword'],
});
export type ProfileFormState = {
message: string;
errors?: {
username?: string[];
email?: string[];
bio?: string[];
currentPassword?: string[];
newPassword?: string[];
confirmNewPassword?: string[];
};
fieldValues?: {
username?: string;
email?: string;
bio?: string;
};
};
Server Actions
Các action này sẽ xử lý các thay đổi dữ liệu thực tế.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { profileSchema, passwordSchema, ProfileFormState } from '@/lib/schemas';
import { z } from 'zod';
// Simulate a database
const usersDb = new Map<string, { username: string; email: string; bio?: string; passwordHash: string }>();
usersDb.set('user123', { username: 'user123', email: 'user@example.com', bio: 'Initial bio.', passwordHash: 'hashed_password_123' });
export async function updateProfile(
prevState: ProfileFormState,
formData: FormData
): Promise<ProfileFormState> {
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate network delay
const rawFormData = {
username: formData.get('username'),
email: formData.get('email'),
bio: formData.get('bio'),
};
const validatedFields = profileSchema.safeParse(rawFormData);
if (!validatedFields.success) {
const fieldErrors = validatedFields.error.flatten().fieldErrors;
return {
message: 'Validation failed.',
errors: fieldErrors,
fieldValues: rawFormData as any, // Return raw values for re-population
};
}
const { username, email, bio } = validatedFields.data;
// Simulate DB update
const currentUser = usersDb.get('user123'); // In real app, get from session
if (currentUser) {
currentUser.username = username;
currentUser.email = email;
currentUser.bio = bio;
usersDb.set('user123', currentUser);
} else {
return { message: 'User not found.', errors: {} };
}
revalidatePath('/profile'); // Invalidate cache for the profile page
return { message: 'Profile updated successfully!', fieldValues: { username, email, bio } };
}
export async function changePassword(
prevState: ProfileFormState,
formData: FormData
): Promise<ProfileFormState> {
await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate network delay
const rawFormData = {
currentPassword: formData.get('currentPassword'),
newPassword: formData.get('newPassword'),
confirmNewPassword: formData.get('confirmNewPassword'),
};
const validatedFields = passwordSchema.safeParse(rawFormData);
if (!validatedFields.success) {
const fieldErrors = validatedFields.error.flatten().fieldErrors;
return {
message: 'Password validation failed.',
errors: fieldErrors,
};
}
const { currentPassword, newPassword } = validatedFields.data;
// Simulate current password check
const currentUser = usersDb.get('user123');
if (!currentUser || currentUser.passwordHash !== 'hashed_password_123') { // In real app, hash and compare
return { message: 'Incorrect current password.', errors: { currentPassword: ['Incorrect password.'] } };
}
// Simulate password update
currentUser.passwordHash = `hashed_${newPassword}`; // In real app, hash new password
usersDb.set('user123', currentUser);
revalidatePath('/profile/security');
return { message: 'Password updated successfully!' };
}
Client Component: Profile Form
Component này sử dụng useActionState, useOptimistic và useFormStatus.
// app/profile/page.tsx
'use client';
import { useActionState, useOptimistic, useRef, useEffect, useState } from 'react';
import { useFormStatus } from 'react-dom';
import { updateProfile, changePassword } from '@/app/actions';
import { ProfileFormState } from '@/lib/schemas';
// Initial state for useActionState
const initialProfileState: ProfileFormState = {
message: '',
errors: {},
fieldValues: { username: 'user123', email: 'user@example.com', bio: 'Initial bio.' },
};
const initialPasswordState: ProfileFormState = {
message: '',
errors: {},
};
function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" aria-disabled={pending} disabled={pending} className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50">
{pending ? 'Saving...' : label}
</button>
);
}
export default function ProfileSettings() {
const [profileState, profileAction] = useActionState(updateProfile, initialProfileState);
const [passwordState, passwordAction] = useActionState(changePassword, initialPasswordState);
// Optimistic state for profile updates
const [optimisticProfile, addOptimisticProfile] = useOptimistic(
profileState.fieldValues,
(currentValues, newValues: Partial<typeof profileState.fieldValues>) => ({
...currentValues,
...newValues,
})
);
const profileFormRef = useRef<HTMLFormElement>(null);
const passwordFormRef = useRef<HTMLFormElement>(null);
// Effect to reset form fields or show success messages
useEffect(() => {
if (profileState.message === 'Profile updated successfully!') {
// Optionally clear form or show a toast
console.log('Profile update success:', profileState.message);
}
if (passwordState.message === 'Password updated successfully!') {
passwordFormRef.current?.reset(); // Clear password fields
console.log('Password update success:', passwordState.message);
}
}, [profileState, passwordState]);
const handleProfileSubmit = async (formData: FormData) => {
// Optimistically update UI
addOptimisticProfile({
username: formData.get('username') as string,
email: formData.get('email') as string,
bio: formData.get('bio') as string,
});
await profileAction(formData);
};
const [currentStep, setCurrentStep] = useState(1);
return (
<div className="max-w-2xl mx-auto p-6 bg-white shadow-md rounded-lg">
<h1 className="text-2xl font-bold mb-6">Profile Settings</h1>
{/* Step 1: Profile Details */}
{currentStep === 1 && (
<section className="mb-8">
<h2 className="text-xl font-semibold mb-4">Update Profile</h2>
<form ref={profileFormRef} action={handleProfileSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Username</label>
<input
type="text"
id="username"
name="username"
defaultValue={optimisticProfile?.username || ''}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
/>
{profileState.errors?.username && (
<p className="text-red-500 text-sm mt-1">{profileState.errors.username[0]}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
id="email"
name="email"
defaultValue={optimisticProfile?.email || ''}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
/>
{profileState.errors?.email && (
<p className="text-red-500 text-sm mt-1">{profileState.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="bio" className="block text-sm font-medium text-gray-700">Bio</label>
<textarea
id="bio"
name="bio"
rows={3}
defaultValue={optimisticProfile?.bio || ''}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
></textarea>
{profileState.errors?.bio && (
<p className="text-red-500 text-sm mt-1">{profileState.errors.bio[0]}</p>
)}
</div>
{profileState.message && profileState.message !== 'Validation failed.' && (
<p className={`text-sm ${profileState.message.includes('successfully') ? 'text-green-600' : 'text-red-600'}`}>
{profileState.message}
</p>
)}
<div className="flex justify-between items-center">
<SubmitButton label="Save Profile" />
<button
type="button"
onClick={() => setCurrentStep(2)}
className="text-blue-600 hover:underline"
>
Next: Change Password →
</button>
</div>
</form>
</section>
)}
{/* Step 2: Change Password */}
{currentStep === 2 && (
<section>
<h2 className="text-xl font-semibold mb-4">Change Password</h2>
<form ref={passwordFormRef} action={passwordAction} className="space-y-4">
<div>
<label htmlFor="currentPassword" className="block text-sm font-medium text-gray-700">Current Password</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
/>
{passwordState.errors?.currentPassword && (
<p className="text-red-500 text-sm mt-1">{passwordState.errors.currentPassword[0]}</p>
)}
</div>
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700">New Password</label>
<input
type="password"
id="newPassword"
name="newPassword"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
/>
{passwordState.errors?.newPassword && (
<p className="text-red-500 text-sm mt-1">{passwordState.errors.newPassword[0]}</p>
)}
</div>
<div>
<label htmlFor="confirmNewPassword" className="block text-sm font-medium text-gray-700">Confirm New Password</label>
<input
type="password"
id="confirmNewPassword"
name="confirmNewPassword"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
/>
{passwordState.errors?.confirmNewPassword && (
<p className="text-red-500 text-sm mt-1">{passwordState.errors.confirmNewPassword[0]}</p>
)}
</div>
{passwordState.message && passwordState.message !== 'Password validation failed.' && (
<p className={`text-sm ${passwordState.message.includes('successfully') ? 'text-green-600' : 'text-red-600'}`}>
{passwordState.message}
</p>
)}
<div className="flex justify-between items-center">
<button
type="button"
onClick={() => setCurrentStep(1)}
className="text-blue-600 hover:underline"
>
← Previous: Profile Details
</button>
<SubmitButton label="Change Password" />
</div>
</form>
</section>
)}
</div>
);
}
Giải thích các Component
ProfileSettings: Đây là một client component ('use client').useActionState(updateProfile, initialProfileState): Khởi tạo trạng thái cho hành động cập nhật hồ sơ.profileStategiữ kết quả cuối cùng (thông báo, lỗi, giá trị trường), vàprofileActionlà hàm dispatch.useOptimistic(profileState.fieldValues, ...): TạooptimisticProfile. KhiaddOptimisticProfileđược gọi,optimisticProfilecập nhật ngay lập tức. NếuprofileActionthất bại,optimisticProfiletrở lạiprofileState.fieldValues. Nếu thành công,profileState.fieldValuescập nhật, vàoptimisticProfiletự nhiên phản ánh trạng thái xác nhận mới.useFormStatus(): Được sử dụng trongSubmitButtonđể vô hiệu hóa nút trong quá trình gửi biểu mẫu, ngăn chặn việc gửi hai lần.- Hook
useEffect: Xóa các trường mật khẩu khi thay đổi mật khẩu thành công và ghi nhật ký thông báo. - Biểu mẫu nhiều bước: Trạng thái
currentStepquản lý phần nào của biểu mẫu hiển thị. - Hiển thị lỗi:
profileState.errorsvàpasswordState.errorsđược hiển thị bên dưới các trường nhập liệu tương ứng của chúng. defaultValue: Quan trọng là,defaultValueđược sử dụng cho các input để cho phép React quản lý trạng thái input một cách hiệu quả vớiuseOptimisticvàuseActionState's trả vềfieldValues.
So sánh kiến trúc: Actions so với các cuộc gọi API truyền thống
| Tính năng | React Actions (Next.js App Router) | REST/GraphQL truyền thống (Client-side Fetch) |
|---|---|---|
| Luồng dữ liệu | Gọi hàm trực tiếp (giống RPC) từ client đến server. | Yêu cầu HTTP (GET/POST/PUT/DELETE) từ client đến endpoint API. |
| Xác thực | Xác thực phía server (ví dụ: Zod) trực tiếp trong action. | Xác thực phía server trong API handler. Xác thực phía client thường bị trùng lặp. |
| Quản lý trạng thái | useActionState, useOptimistic, useFormStatus cho trạng thái thay đổi. | useState, useReducer, React Query/SWR cho trạng thái thay đổi. |
| Xác thực lại | revalidatePath, revalidateTag để tự động hủy bộ nhớ đệm. | Hủy bộ nhớ đệm thủ công (ví dụ: queryClient.invalidateQueries). |
| UI lạc quan | Hook useOptimistic tích hợp sẵn. | Triển khai thủ công với useState và logic hoàn tác. |
| Xử lý lỗi | Action trả về đối tượng trạng thái với lỗi. | Khối catch trên fetch hoặc xử lý lỗi cụ thể của thư viện. |
| Lớp mạng | Được trừu tượng hóa bởi React/Next.js. | Các cuộc gọi fetch hoặc Axios rõ ràng. |
| Kích thước gói | Gói client có thể nhỏ hơn vì logic server được loại bỏ. | Gói client bao gồm các thư viện tìm nạp dữ liệu và quản lý trạng thái. |
| Độ phức tạp | Đường cong học tập ban đầu cho mô hình tư duy mới. | Mô hình yêu cầu HTTP quen thuộc. |
Những vấn đề và cách khắc phục trong sản xuất
-
Lỗi "Actions must be functions":
- Triệu chứng:
TypeError: Actions must be functions. Received: [object Object]hoặc tương tự. - Nguyên nhân: Quên
'use server'ở đầu tệp hoặc hàm action của bạn. Hoặc, cố gắng truyền một non-function làm prop action cho một<form>. - Cách khắc phục: Đảm bảo tất cả các server action được đánh dấu chính xác bằng
'use server'. Xác minh prop action trên<form>của bạn thực sự là một hàm.
- Triệu chứng:
-
revalidatePath/revalidateTagKhông hoạt động:- Triệu chứng: Dữ liệu trên trang không cập nhật sau một action thành công, ngay cả với
revalidatePath. - Nguyên nhân:
- Đường dẫn đang được xác thực lại không được Next.js lưu vào bộ nhớ đệm (ví dụ: đó là một dynamic route không được truy cập theo cách cho phép lưu vào bộ nhớ đệm, hoặc đó là một client component tìm nạp dữ liệu phía client).
- Action được gọi từ một client component, và việc xác thực lại xảy ra, nhưng bản thân client component không tìm nạp lại dữ liệu của nó.
- Sử dụng
fetchmà không cócache: 'force-cache'hoặcnext.tags.
- Cách khắc phục:
- Đảm bảo dữ liệu bạn mong muốn xác thực lại được tìm nạp trong một Server Component hoặc một Server Action sử dụng
fetchvới các tiêu đề bộ nhớ đệm thích hợp hoặcnext.tags. - Đối với các client component, đảm bảo chúng nhận được props từ một Server Component có xác thực lại, hoặc kích hoạt tìm nạp lại phía client sau khi action hoàn thành (mặc dù điều này thường làm mất đi mục đích của Actions).
- Xác minh đối số
revalidatePathkhớp với đường dẫn thực tế đang được render.
- Đảm bảo dữ liệu bạn mong muốn xác thực lại được tìm nạp trong một Server Component hoặc một Server Action sử dụng
- Triệu chứng: Dữ liệu trên trang không cập nhật sau một action thành công, ngay cả với
-
Cập nhật lạc quan không hoàn tác:
- Triệu chứng: UI hiển thị trạng thái lạc quan ngay cả sau khi server action thất bại.
- Nguyên nhân:
useActionState's trả vềstate(màuseOptimisticsử dụng làm cơ sở) không phản ánh chính xác sự thất bại. Điều này thường xảy ra nếu server action ném ra một lỗi không được xử lý thay vì trả về một trạng thái lỗi. - Cách khắc phục: Đảm bảo server action của bạn luôn trả về một đối tượng
ProfileFormState(hoặc tương đương), ngay cả khi thất bại. Bắt lỗi trong action và trả về một thông báo lỗi thích hợp và đối tượngerrors.useOptimisticdựa vào trạng thái cơ sở từuseActionStateđể hoàn tác.
-
useFormStatusKhông cập nhật:- Triệu chứng: Nút gửi vẫn được bật/tắt không chính xác.
- Nguyên nhân:
useFormStatusphải được sử dụng trong một component là con trực tiếp của một phần tử<form>, hoặc một component được render trong một phần tử<form>. Nó không thể đọc trạng thái của một biểu mẫu cao hơn trong cây nếu có các component khác ở giữa làm hỏng ngữ cảnh. - Cách khắc phục: Đặt component sử dụng
useFormStatus(nhưSubmitButtoncủa chúng ta) trực tiếp bên trong<form>hoặc đảm bảo cha của nó nằm trực tiếp bên trong<form>.
-
Vấn đề
FormDatavới các đối tượng phức tạp:- Triệu chứng: Không thể trực tiếp truyền các đối tượng JavaScript phức tạp (ví dụ: mảng lồng nhau, đối tượng) qua
FormData. - Nguyên nhân:
FormDatađược thiết kế cho các cặp khóa-giá trị đơn giản, chủ yếu là chuỗi hoặc đối tượngFile. Nó làm phẳng các cấu trúc phức tạp. - Cách khắc phục: Đối với dữ liệu phức tạp, hãy cân nhắc tuần tự hóa nó thành JSON và thêm nó dưới dạng một trường chuỗi duy nhất vào
FormData, sau đó phân tích cú pháp nó trên máy chủ. Ngoài ra, nếu không sử dụng phần tử<form>, bạn có thể trực tiếp truyền đối số cho một server action.
tsx// Client component import { myComplexAction } from './actions'; async function handleSubmit(data: MyComplexDataType) { await myComplexAction(data); // Direct argument passing }typescript// Server action 'use server'; import { MyComplexDataType } from '@/lib/types'; export async function myComplexAction(data: MyComplexDataType) { // Process complex data directly } - Triệu chứng: Không thể trực tiếp truyền các đối tượng JavaScript phức tạp (ví dụ: mảng lồng nhau, đối tượng) qua
Các câu hỏi thường gặp
1. Khi nào tôi nên sử dụng useActionState so với việc gọi trực tiếp một server action?
useActionState lý tưởng khi bạn cần quản lý trạng thái được trả về bởi một server action, bao gồm trạng thái đang chờ xử lý, thông báo lỗi và kết quả thành công, và có các trạng thái này kích hoạt re-render trong component của bạn. Nó cung cấp một cách có cấu trúc để xử lý vòng đời của action. Việc gọi trực tiếp một server action (ví dụ: await myAction(formData)) phù hợp cho các kịch bản đơn giản hơn khi bạn có thể không cần đầy đủ khả năng quản lý trạng thái, hoặc khi tích hợp với các giải pháp quản lý trạng thái khác.
2. Tôi có thể sử dụng React Actions với các thư viện định tuyến phía client như React Router không?
React Actions chủ yếu được thiết kế để tích hợp sâu với mô hình render của React và App Router của Next.js cho các tương tác phía máy chủ và xác thực lại dữ liệu. Mặc dù về mặt kỹ thuật, bạn có thể gọi một server action từ một component trong một ứng dụng React Router, bạn sẽ mất đi lợi ích xác thực lại tự động của revalidatePath và revalidateTag, và sẽ cần quản lý thủ công việc tìm nạp và lưu vào bộ nhớ đệm dữ liệu. Đối với Next.js App Router, Actions là cách tiếp cận chuẩn mực.
3. Làm cách nào để xử lý các lỗi toàn cục hoặc các lỗi máy chủ không mong muốn từ một action?
Các server action lý tưởng nên tự bắt lỗi của chúng và trả về một trạng thái lỗi có cấu trúc (như đã trình bày với ProfileFormState). Đối với các ngoại lệ không được xử lý hoặc các lỗi máy chủ nghiêm trọng, Next.js thường sẽ hiển thị một trang lỗi (error.tsx hoặc global-error.tsx). Bạn cũng có thể triển khai các ranh giới lỗi phía client để bắt lỗi trong giai đoạn thực thi của action, mặc dù bản thân action nên cố gắng trả về một trạng thái có thể dự đoán được. Đối với các lỗi cấp độ mạng, useActionState cuối cùng sẽ giải quyết bằng một lỗi, sau đó bạn có thể hiển thị.
4. Có an toàn không khi truyền dữ liệu nhạy cảm qua FormData đến một server action?
Có, khi sử dụng <form action={serverAction}>, FormData được gửi qua một yêu cầu POST. Điều này an toàn như bất kỳ yêu cầu POST nào khác đến máy chủ của bạn, miễn là kết nối của bạn là HTTPS. Dữ liệu không bị lộ trong URL. Luôn xác thực và làm sạch tất cả dữ liệu đến trên máy chủ, bất kể nó được truyền như thế nào.
5. Actions ảnh hưởng đến kích thước gói và hiệu suất như thế nào?
Bản thân Server Actions không làm tăng kích thước gói JavaScript phía client vì mã của chúng chỉ chạy trên máy chủ. React tự động tạo một stub phía client nhỏ để gọi hàm server. Điều này có thể dẫn đến các gói client nhỏ hơn so với việc tìm nạp dữ liệu phía client truyền thống, nơi các thư viện client API và logic quản lý trạng thái phức tạp có thể được bao gồm. Hiệu suất thường được cải thiện do giảm JavaScript phía client, ít vòng lặp mạng hơn (vì các thay đổi dữ liệu và xác thực lại thường được nhóm lại), và truy cập cơ sở dữ liệu trực tiếp từ máy chủ.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

React 19: Server Actions & Cập nhật Optimistic (Không độ trễ)
Làm chủ useOptimistic và Server Actions với cơ chế rollback tự động khi có lỗi mạng. Bao gồm ví dụ code production, các mẫu transition, và biểu đồ tuần tự.
Read more
TanStack Query v5 với Next.js 15: Cập nhật lạc quan, đồng bộ hóa bộ nhớ đệm & Server Actions
Hướng dẫn toàn diện về TanStack Query v5 với Next.js 15: cập nhật lạc quan, đồng bộ hóa bộ nhớ đệm và Server Actions với kiến trúc cấp độ sản xuất và ví dụ mã.
Read more
React Compiler trong Thực tế: Tự động Memoization, Quy tắc & Điểm chuẩn Hiệu suất
Hướng dẫn toàn diện về React Compiler trong thực tế: tự động memoization, quy tắc và điểm chuẩn hiệu suất với kiến trúc cấp độ production và ví dụ code.
Read more