•18 min read

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

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

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ẽ.

Audio Briefing
0:00 / 0:00

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.
Advertisement

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

  1. Tương tác Client: Người dùng gửi biểu mẫu hoặc kích hoạt một action.
  2. Gọi Action: Hàm action được gọi.
  3. 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ả.
  4. 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 &rarr;
              </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"
              >
                &larr; 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ơ. profileState giữ kết quả cuối cùng (thông báo, lỗi, giá trị trường), và profileAction là hàm dispatch.
    • useOptimistic(profileState.fieldValues, ...): Tạo optimisticProfile. Khi addOptimisticProfile được gọi, optimisticProfile cập nhật ngay lập tức. Nếu profileAction thất bại, optimisticProfile trở lại profileState.fieldValues. Nếu thành công, profileState.fieldValues cập nhật, và optimisticProfile tự nhiên phản ánh trạng thái xác nhận mới.
    • useFormStatus(): Được sử dụng trong SubmitButton để 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 currentStep quản lý phần nào của biểu mẫu hiển thị.
    • Hiển thị lỗi: profileState.errors và 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ới useOptimistic và 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ăngReact Actions (Next.js App Router)REST/GraphQL truyền thống (Client-side Fetch)
Luồng dữ liệuGọ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ựcXá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áiuseActionState, 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ạirevalidatePath, revalidateTag để tự động hủy bộ nhớ đệm.Hủy bộ nhớ đệm thủ công (ví dụ: queryClient.invalidateQueries).
UI lạc quanHook 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ỗiAction 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óiGó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.
Advertisement

Những vấn đề và cách khắc phục trong sản xuất

  1. 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.
  2. revalidatePath / revalidateTag Khô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 fetch mà không có cache: 'force-cache' hoặc next.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 fetch với các tiêu đề bộ nhớ đệm thích hợp hoặc next.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ố revalidatePath khớp với đường dẫn thực tế đang được render.
  3. 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à useOptimistic sử 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ượng errors. useOptimistic dựa vào trạng thái cơ sở từ useActionState để hoàn tác.
  4. useFormStatus Khô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: useFormStatus phả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ư SubmitButton củ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>.
  5. Vấn đề FormData vớ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ượng File. 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.
    // Client component
    import { myComplexAction } from './actions';
    
    async function handleSubmit(data: MyComplexDataType) {
      await myComplexAction(data); // Direct argument passing
    }
    
    // Server action
    'use server';
    import { MyComplexDataType } from '@/lib/types';
    
    export async function myComplexAction(data: MyComplexDataType) {
      // Process complex data directly
    }
    

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ủ.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement