TanStack Query v5 với Next.js 15: Cập nhật lạc quan, đồng bộ hóa bộ nhớ đệm & Server Actions

Mục lục bài viết(9 mục)
Hướng dẫn này trình bày chi tiết việc tích hợp TanStack Query v5 với Next.js 15 App Router và Server Actions, tập trung vào các mẫu nâng cao cho UI lạc quan (optimistic UI), đồng bộ hóa bộ nhớ đệm và tìm nạp dữ liệu hiệu quả. Chúng ta sẽ đề cập đến các cập nhật lạc quan bền vững với khả năng hoàn tác tự động, khử/khôi phục trạng thái phía máy chủ và triển khai cuộn vô hạn mạnh mẽ.
Lộ trình Kiến trúc Next.js 15 & React 19
Tổng quan kiến trúc
Next.js 15 App Router và Server Actions giới thiệu một sự thay đổi mô hình trong việc tìm nạp và thay đổi dữ liệu. Server Actions cho phép gọi trực tiếp các hàm phía máy chủ từ các thành phần client, đơn giản hóa việc thay đổi dữ liệu. Ngược lại, TanStack Query quản lý việc tìm nạp, lưu trữ và đồng bộ hóa dữ liệu phía client. Thách thức nằm ở việc điều phối hai hệ thống này để cung cấp trải nghiệm người dùng nhất quán, hiệu suất cao, đặc biệt khi xử lý các thay đổi ảnh hưởng đến dữ liệu dùng chung.
Nguyên tắc cốt lõi là tận dụng Server Actions cho các thay đổi và revalidatePath/revalidateTag để vô hiệu hóa bộ nhớ đệm phía máy chủ, đồng thời sử dụng TanStack Query để quản lý dữ liệu phía client, bao gồm các cập nhật lạc quan và vô hiệu hóa bộ nhớ đệm cục bộ.
Thiết lập ban đầu và khử nước
Để đảm bảo trải nghiệm người dùng liền mạch, đặc biệt là khi tải trang ban đầu, chúng ta khử nước bộ nhớ đệm TanStack Query trên máy chủ và khôi phục nó trên client. Điều này ngăn chặn việc hiển thị biểu tượng tải trên lần hiển thị đầu tiên đối với dữ liệu có thể đã được tìm nạp trong quá trình SSR/SSG.
Đầu tiên, thiết lập QueryClientProvider và HydrationBoundary.
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import React from 'react';
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Aggressively refetch on mount to ensure data freshness,
// especially after server-side revalidation.
staleTime: 5 * 1000, // 5 seconds
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
},
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
</QueryClientProvider>
);
}
Sau đó, trong bố cục gốc hoặc một trang cụ thể, tìm nạp trước dữ liệu và khử nước trạng thái.
import { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { Providers } from './providers';
import { getPosts } from '@/lib/data'; // Example data fetching function
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const queryClient = new QueryClient();
// Prefetch data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
});
return (
<html lang="en">
<body>
<Providers>
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
</Providers>
</body>
</html>
);
}
Lưu ý HydrationBoundary lồng nhau. Cái bên ngoài trong layout.tsx khôi phục dữ liệu được tìm nạp trước trong quá trình SSR cho lần tải trang ban đầu. Cái bên trong trong providers.tsx về mặt kỹ thuật là thừa cho lần tải ban đầu nhưng đảm bảo rằng mọi điều hướng phía client hoặc gắn kết thành phần tiếp theo trong ngữ cảnh Providers vẫn có thể tận dụng quá trình khôi phục nếu dehydrate(queryClient) được gọi lại trong một ngữ cảnh khác (ví dụ: một bố cục lồng nhau). Đối với một thiết lập điển hình, HydrationBoundary bên ngoài là đủ.
Cập nhật lạc quan với Server Actions
Các cập nhật lạc quan cung cấp phản hồi UI ngay lập tức, nâng cao hiệu suất cảm nhận. Khi người dùng thực hiện một hành động (ví dụ: thêm một bài đăng), UI sẽ cập nhật ngay lập tức, giả sử hành động sẽ thành công. Nếu hành động máy chủ thất bại, UI sẽ hoàn tác về trạng thái trước đó.
Hãy xem xét một kịch bản trong đó người dùng có thể thêm bài đăng mới.
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { Post } from '@/lib/types'; // Assume Post type is defined
let posts: Post[] = [
{ id: '1', title: 'Initial Post', content: 'This is the first post.' },
];
let nextId = 2;
export async function addPostAction(title: string, content: string): Promise<Post> {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000 + 500));
// Simulate an error 20% of the time
if (Math.random() < 0.2) {
throw new Error('Failed to add post: Network error or server issue.');
}
const newPost: Post = { id: String(nextId++), title, content };
posts.unshift(newPost); // Add to the beginning for easier optimistic update
console.log('Server: Added post', newPost);
// Invalidate Next.js cache for the path where posts are displayed
revalidatePath('/posts');
revalidateTag('posts'); // Invalidate a specific tag if used
return newPost;
}
export async function getPosts(): Promise<Post[]> {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 300));
return posts;
}
Bây giờ, thành phần client:
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { addPostAction, getPosts } from '@/app/actions';
import { Post } from '@/lib/types'; // Assume Post type is defined
import { useState } from 'react';
export default function PostsPage() {
const queryClient = useQueryClient();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const { data: posts, isLoading, isError, error } = useQuery<Post[]>({
queryKey: ['posts'],
queryFn: getPosts,
});
const addPostMutation = useMutation({
mutationFn: async ({ title, content }: { title: string; content: string }) => {
return addPostAction(title, content);
},
onMutate: async (newPostData) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot the previous value
const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
// Optimistically update to the new value
queryClient.setQueryData<Post[]>(['posts'], (old) => {
const tempId = `optimistic-${Date.now()}`; // Temporary ID for optimistic item
const optimisticPost: Post = { ...newPostData, id: tempId };
return old ? [optimisticPost, ...old] : [optimisticPost];
});
// Return a context object with the snapshotted value
return { previousPosts };
},
onError: (err, newPostData, context) => {
// If the mutation fails, use the context for an immediate rollback
console.error('Optimistic update failed:', err);
queryClient.setQueryData(['posts'], context?.previousPosts);
// Optionally, show a toast notification
alert(`Failed to add post: ${err.message}`);
},
onSettled: () => {
// Always refetch after error or success:
// This ensures our client-side cache is eventually consistent with the server.
// It also handles cases where the server-side revalidation might not immediately
// propagate to the client's data fetching mechanism (e.g., if the client
// is still using stale data from its own cache).
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
onSuccess: (data) => {
// Optionally, if the server returns the full updated list or a specific ID,
// you could update the optimistic item with the real ID.
// For simplicity, `onSettled`'s `invalidateQueries` handles this.
console.log('Post added successfully:', data);
setTitle('');
setContent('');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title || !content) return;
addPostMutation.mutate({ title, content });
};
if (isLoading) return <div>Loading posts...</div>;
if (isError) return <div>Error: {error?.message}</div>;
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Posts</h1>
<form onSubmit={handleSubmit} className="mb-8 p-4 border rounded-lg shadow-sm">
<h2 className="text-xl font-semibold mb-3">Add New Post</h2>
<div className="mb-3">
<label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2"
disabled={addPostMutation.isPending}
/>
</div>
<div className="mb-3">
<label htmlFor="content" className="block text-sm font-medium text-gray-700">Content</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={3}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2"
disabled={addPostMutation.isPending}
></textarea>
</div>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
disabled={addPostMutation.isPending}
>
{addPostMutation.isPending ? 'Adding...' : 'Add Post'}
</button>
{addPostMutation.isError && (
<p className="text-red-500 text-sm mt-2">Error: {addPostMutation.error?.message}</p>
)}
</form>
<div className="space-y-4">
{posts?.map((post) => (
<div key={post.id} className="p-4 border rounded-lg shadow-sm bg-white">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p className="text-gray-600">{post.content}</p>
</div>
))}
</div>
</div>
);
}
Các khía cạnh chính của cập nhật lạc quan:
-
onMutate:queryClient.cancelQueries: Ngăn chặn mọi tìm nạp lại nền đang diễn ra ghi đè lên cập nhật lạc quan của chúng ta.queryClient.getQueryData: Chụp nhanh dữ liệu hiện tại trước khi sửa đổi. Điều này rất quan trọng để hoàn tác.queryClient.setQueryData: Cập nhật ngay lập tức bộ nhớ đệm phía client với mục mới, được thêm vào một cách lạc quan. Một ID tạm thời (optimistic-${Date.now()}) được sử dụng để phân biệt nó với các mục được máy chủ xác nhận.- Trả về
previousPoststrong một đối tượng ngữ cảnh, được truyền đếnonError.
-
onError:- Nếu
addPostActionthất bại,onErrorđược gọi. queryClient.setQueryDatađược sử dụng vớicontext.previousPostsđể khôi phục bộ nhớ đệm phía client về trạng thái trước khi cập nhật lạc quan.
- Nếu
-
onSettled:- Callback này chạy bất kể thành công hay thất bại.
queryClient.invalidateQueries({ queryKey: ['posts'] }): Đánh dấu truy vấn['posts']là cũ. Điều này kích hoạt tìm nạp lại nền, đảm bảo bộ nhớ đệm phía client cuối cùng phản ánh trạng thái máy chủ thực. Điều này rất quan trọng để đồng bộ hóa vớirevalidatePath/revalidateTagtrên máy chủ. Ngay cả khi máy chủ đã xác thực lại bộ nhớ đệm thành công, client cần tìm nạp lại để lấy dữ liệu mới nhất.
Cuộn vô hạn với useInfiniteQuery
Việc triển khai cuộn vô hạn đòi hỏi phải xử lý cẩn thận việc tổng hợp dữ liệu và ngăn chặn sự không khớp khi khôi phục. useInfiniteQuery từ TanStack Query là lý tưởng cho việc này.
// ... (previous actions)
export async function getPaginatedPosts(pageParam: number, limit: number = 5): Promise<{ posts: Post[]; nextPage: number | undefined }> {
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate network delay
const startIndex = (pageParam - 1) * limit;
const endIndex = startIndex + limit;
const paginatedPosts = posts.slice(startIndex, endIndex);
const nextPage = endIndex < posts.length ? pageParam + 1 : undefined;
return { posts: paginatedPosts, nextPage };
}
'use client';
import { useInfiniteQuery } from '@tanstack/react-query';
import { getPaginatedPosts } from '@/app/actions';
import { Post } from '@/lib/types';
import { useEffect, useRef } from 'react';
export default function InfinitePostsPage() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
error,
} = useInfiniteQuery({
queryKey: ['infinitePosts'],
queryFn: ({ pageParam }) => getPaginatedPosts(pageParam as number),
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60, // 1 hour
});
const observerTarget = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!observerTarget.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 1 }
);
observer.observe(observerTarget.current);
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current);
}
};
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
if (isLoading) return <div>Loading infinite posts...</div>;
if (isError) return <div>Error: {error?.message}</div>;
const allPosts = data?.pages.flatMap((page) => page.posts) || [];
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Infinite Posts</h1>
<div className="space-y-4">
{allPosts.map((post) => (
<div key={post.id} className="p-4 border rounded-lg shadow-sm bg-white">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p className="text-gray-600">{post.content}</p>
</div>
))}
</div>
<div ref={observerTarget} className="h-1 bg-transparent my-4" />
{isFetchingNextPage && <div className="text-center py-2">Loading more...</div>}
{!hasNextPage && allPosts.length > 0 && (
<div className="text-center py-2 text-gray-500">No more posts to load.</div>
)}
</div>
);
}
Các cân nhắc về sự không khớp khi khôi phục cho cuộn vô hạn
Khi sử dụng useInfiniteQuery với kết xuất phía máy chủ (SSR) hoặc tạo trang tĩnh (SSG), một cạm bẫy phổ biến là sự không khớp khi khôi phục. Nếu bạn chỉ tìm nạp trước trang đầu tiên trên máy chủ, nhưng useInfiniteQuery phía client cố gắng hiển thị nhiều trang hơn (ví dụ: do kết nối nhanh hoặc logic tiền kết xuất), HTML được kết xuất phía máy chủ sẽ khác với HTML được kết xuất phía client.
Chiến lược để tránh sự không khớp khi khôi phục:
-
Chỉ tìm nạp trước trang đầu tiên trên máy chủ. Đây là cách tiếp cận phổ biến và an toàn nhất. Client sau đó sẽ tìm nạp các trang tiếp theo.
tsximport { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query'; import { getPaginatedPosts } from '@/app/actions'; import { Providers } from '@/app/providers'; // Your existing providers export default async function InfinitePostsLayout({ children, }: { children: React.ReactNode; }) { const queryClient = new QueryClient(); // Prefetch only the first page await queryClient.prefetchInfiniteQuery({ queryKey: ['infinitePosts'], queryFn: ({ pageParam }) => getPaginatedPosts(pageParam as number), initialPageParam: 1, }); return ( <Providers> <HydrationBoundary state={dehydrate(queryClient)}> {children} </HydrationBoundary> </Providers> ); }Điều này đảm bảo lần hiển thị ban đầu nhất quán. Các trang tiếp theo được tìm nạp phía client.
-
Tránh tìm nạp trước nhiều trang trên máy chủ trừ khi thực sự cần thiết và bạn có thể đảm bảo kết xuất nhất quán. Nếu bạn tìm nạp trước nhiều trang, hãy đảm bảo logic kết xuất phía client cho
useInfiniteQuerycó thể xây dựng lại chính xác cùng một cấu trúc HTML dựa trên trạng thái đã khử nước. Điều này phức tạp và thường dẫn đến các vấn đề.
Đồng bộ hóa bộ nhớ đệm: revalidatePath so với revalidateTag
Next.js 15 cung cấp hai phương pháp chính để vô hiệu hóa bộ nhớ đệm dữ liệu của nó:
revalidatePath(path: string): Vô hiệu hóa bộ nhớ đệm cho một đường dẫn cụ thể. Khi đường dẫn được yêu cầu tiếp theo, nó sẽ được kết xuất lại. Hữu ích cho các trang hiển thị dữ liệu từ một nguồn duy nhất.revalidateTag(tag: string): Vô hiệu hóa bộ nhớ đệm cho tất cả dữ liệu được tìm nạp bằng một thẻ cụ thể. Điều này chi tiết và mạnh mẽ hơn, cho phép vô hiệu hóa dữ liệu trên nhiều đường dẫn chia sẻ một nguồn dữ liệu chung (ví dụ: tất cả bài đăng, tất cả sản phẩm).
Khi nào nên sử dụng cái nào:
| Tính năng | revalidatePath | revalidateTag |
|---|---|---|
| Mức độ chi tiết | Cấp độ trang | Cấp độ dữ liệu (trên các trang) |
| Trường hợp sử dụng | Cập nhật một trang cụ thể (ví dụ: /blog/post-123) | Cập nhật tất cả các trang hiển thị 'bài đăng' hoặc 'sản phẩm' |
| Thiết lập | Tự động cho các lệnh gọi fetch trong một đường dẫn | Yêu cầu gắn thẻ các lệnh gọi fetch |
| Độ phức tạp | Đơn giản hơn cho các trang riêng biệt | Thiết lập nhiều hơn, nhưng linh hoạt hơn cho dữ liệu dùng chung |
| Tác động | Kết xuất lại đường dẫn được chỉ định trong yêu cầu tiếp theo | Kết xuất lại tất cả các đường dẫn sử dụng thẻ bị vô hiệu hóa |
Ví dụ với revalidateTag:
Để sử dụng revalidateTag, bạn phải gắn thẻ các yêu cầu fetch của mình. Nếu bạn đang sử dụng một lớp tìm nạp dữ liệu tùy chỉnh (như ví dụ getPosts của chúng ta), bạn thường sẽ bao bọc fetch hoặc sử dụng một thư viện hỗ trợ gắn thẻ. Đối với Server Actions, revalidateTag hoạt động trực tiếp.
// ... (previous actions)
// Example of how you might tag a fetch request if you were fetching directly
// async function getPostsFromAPI(): Promise<Post[]> {
// const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] } });
// if (!res.ok) throw new Error('Failed to fetch posts');
// return res.json();
// }
// Our current `getPosts` is a direct function call, so `revalidateTag`
// in `addPostAction` directly invalidates the tag, assuming Next.js
// tracks this for Server Actions. For `fetch` calls, explicit tagging is needed.
// For simplicity in this example, `revalidateTag('posts')` is called,
// implying that any data fetching mechanism that Next.js tracks under 'posts'
// will be invalidated. In a real app, ensure your data fetching is tagged.
Trong addPostAction của chúng ta, chúng ta sử dụng cả revalidatePath('/posts') và revalidateTag('posts'). Điều này cung cấp một chiến lược vô hiệu hóa mạnh mẽ:
revalidatePath('/posts')đảm bảo rằng trang/postscụ thể được kết xuất lại trong yêu cầu tiếp theo.revalidateTag('posts')sẽ vô hiệu hóa bất kỳ trang hoặc thành phần nào khác có thể đang tìm nạp dữ liệu được gắn thẻ là 'bài đăng'.
Ở phía client, queryClient.invalidateQueries({ queryKey: ['posts'] }) của TanStack Query đảm bảo rằng bộ nhớ đệm của client cho ['posts'] được đánh dấu là cũ, kích hoạt tìm nạp lại. Việc tìm nạp lại này sau đó sẽ truy cập máy chủ Next.js, máy chủ này, do revalidatePath/revalidateTag, sẽ phục vụ dữ liệu mới.
Những vấn đề và cách khắc phục trong sản xuất
-
Dữ liệu cũ sau Server Action:
- Triệu chứng: Người dùng thực hiện một hành động, cập nhật lạc quan hoạt động, nhưng sau khi hành động máy chủ hoàn tất, dữ liệu bị hoàn tác hoặc không cập nhật đúng cách.
- Nguyên nhân: Thiếu hoặc sai
revalidatePath/revalidateTagtrên máy chủ, hoặc thiếuqueryClient.invalidateQueriestrên client. - Khắc phục: Đảm bảo Server Action của bạn gọi rõ ràng
revalidatePathcho (các) trang bị ảnh hưởng và/hoặcrevalidateTagcho các thẻ dữ liệu liên quan. Trên client, luôn gọiqueryClient.invalidateQueriestrongonSettledhoặconSuccesscủauseMutationcủa bạn để buộc tìm nạp lại.
-
Không khớp khi khôi phục (
Warning: PropclassNamedid not match. Server: "..." Client: "..."):- Triệu chứng: Lỗi trong console liên quan đến
hydrationhoặcdid not match. Thường xảy ra với cuộn vô hạn hoặc nội dung động. - Nguyên nhân: HTML được kết xuất trên máy chủ khác với những gì React kết xuất trên client. Điều này có thể xảy ra nếu các hiệu ứng phía client (ví dụ:
useEffectsửa đổi DOM,Date.now(),Math.random()) chạy trước khi khôi phục, hoặc nếuuseInfiniteQuerytìm nạp các lượng dữ liệu khác nhau trên máy chủ so với client. - Khắc phục:
- Đối với cuộn vô hạn, chỉ tìm nạp trước trang đầu tiên trên máy chủ. Để client tìm nạp các trang tiếp theo.
- Đảm bảo bất kỳ thành phần chỉ dành cho client nào có thể gây ra sự không khớp được bao bọc trong
<ClientOnly>hoặc được nhập động vớissr: false. - Tránh logic cụ thể phía client (như
Date.now()) trong các thành phần được kết xuất trên máy chủ.
- Triệu chứng: Lỗi trong console liên quan đến
-
Tìm nạp quá nhiều/quá ít dữ liệu khi tải ban đầu:
- Triệu chứng: Trang tải với biểu tượng tải cho dữ liệu đáng lẽ phải có sẵn, hoặc tìm nạp quá nhiều dữ liệu.
- Nguyên nhân: Thiết lập
prefetchQueryhoặcHydrationBoundarykhông chính xác. - Khắc phục: Xác minh
queryClient.prefetchQueryđược gọi trướcdehydrate(queryClient). Đảm bảoHydrationBoundarybao bọc các thành phần sử dụng dữ liệu được tìm nạp trước. Đối với các truy vấn vô hạn, chỉ tìm nạp trước trang đầu tiên.
-
Các vấn đề hoàn tác cập nhật lạc quan:
- Triệu chứng: Cập nhật lạc quan thất bại, nhưng UI không hoàn tác, hoặc hoàn tác không chính xác.
- Nguyên nhân: Chụp nhanh
onMutatekhông chính xác hoặc logic hoàn táconError. - Khắc phục: Kiểm tra kỹ xem
onMutatecó chụp chính xácpreviousPostsvàonErrorcó sử dụng ảnh chụp nhanh này đểsetQueryDatavề trạng thái ban đầu hay không. Đảm bảoqueryClient.cancelQueriesđược gọi để ngăn chặn các điều kiện tranh chấp.
-
Lỗi Server Action không được truyền đi:
- Triệu chứng: Server Action thất bại, nhưng
useMutation'sonErrorkhông được kích hoạt, hoặc thông báo lỗi chung chung. - Nguyên nhân: Server Action không ném rõ ràng một đối tượng
Error, hoặc lỗi bị bắt và không được ném lại. - Khắc phục: Đảm bảo Server Actions của bạn
throw new Error('...')cho các lỗi. Next.js sau đó sẽ truyền lỗi này một cách chính xác đến callbackonErrorcủauseMutationphía client.
- Triệu chứng: Server Action thất bại, nhưng
Câu hỏi thường gặp
-
Khi nào tôi nên sử dụng
revalidatePathso vớirevalidateTag? Sử dụngrevalidatePathkhi một thay đổi chủ yếu ảnh hưởng đến dữ liệu được hiển thị trên một trang cụ thể, duy nhất. Sử dụngrevalidateTagkhi một thay đổi ảnh hưởng đến dữ liệu có thể được hiển thị trên nhiều trang hoặc thành phần, cho phép vô hiệu hóa dữ liệu dùng chung một cách chi tiết và hiệu quả hơn. Để có độ bền tối đa, bạn có thể sử dụng cả hai nếu có thể, như được hiển thị trong ví dụ. -
Làm cách nào để xử lý xác thực và ủy quyền với Server Actions và TanStack Query? Server Actions chạy trên máy chủ, vì vậy bạn có thể truy cập trực tiếp các ngữ cảnh xác thực phía máy chủ (ví dụ: từ NextAuth.js, Clerk hoặc quản lý phiên tùy chỉnh). Thực hiện kiểm tra ủy quyền trong Server Actions của bạn. Nếu không được ủy quyền, hãy ném lỗi.
useMutationcủa TanStack Query sẽ bắt lỗi này trongonError, cho phép bạn hiển thị phản hồi UI thích hợp. Đối với việc tìm nạp dữ liệu,useQuerycũng có thể gọi Server Actions, có thể thực hiện kiểm tra xác thực. -
Tôi có thể sử dụng TanStack Query cho tất cả việc tìm nạp dữ liệu, ngay cả đối với các lần tải trang ban đầu trên máy chủ không? Vâng, hoàn toàn có thể. Bằng cách sử dụng
queryClient.prefetchQuerytrong Next.js Server Components hoặc các tệplayout.tsx/page.tsxcủa bạn, và sau đó khử nước trạng thái bằngHydrationBoundary, bạn có thể tận dụng bộ nhớ đệm và quản lý dữ liệu của TanStack Query cho cả kết xuất phía máy chủ và phía client, cung cấp một lớp dữ liệu thống nhất. -
Vai trò của
staleTimevàgcTimetrong TanStack Query với Next.js là gì?staleTimeđịnh nghĩa thời gian dữ liệu được coi là "mới". Khi còn mới,useQuerysẽ không tìm nạp lại khi kết xuất lại hoặc gắn kết thành phần. Khi cũ, nó sẽ tìm nạp lại trong nền.gcTime(thời gian thu gom rác) định nghĩa thời gian các truy vấn không hoạt động còn lại trong bộ nhớ đệm trước khi bị thu gom rác. Trong một ứng dụng Next.js,staleTimegiúp giảm các yêu cầu mạng không cần thiết khi điều hướng phía client, trong khigcTimequản lý việc sử dụng bộ nhớ. Điều chỉnh những điều này dựa trên sự biến động của dữ liệu và các ràng buộc bộ nhớ của ứng dụng của bạn. -
Làm cách nào để quản lý các trạng thái biểu mẫu phức tạp với các cập nhật lạc quan? Đối với các biểu mẫu phức tạp, hãy cân nhắc sử dụng một thư viện biểu mẫu như React Hook Form. Khi gửi, hãy sử dụng
useMutationnhư đã mô tả. Thư viện biểu mẫu quản lý trạng thái đầu vào, vàuseMutationxử lý việc gửi, cập nhật lạc quan và hoàn tác lỗi. Xóa các trường biểu mẫu trongonSuccesscủauseMutation.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Next.js 15 Server Actions vs Route Handlers: So sánh kiến trúc chuyên sâu
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, đi sâu vào progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Read more
Next.js Server Actions
Nắm vững Next.js Server Actions để thay đổi dữ liệu toàn diện: khám phá cơ chế biên dịch, hành động biểu mẫu, bộ nhớ đệm revalidatePath và ranh giới bảo mật.
Read more
React 19 Actions trong thực tế: useActionState, useOptimistic & khả năng phục hồi của Server Action
Hướng dẫn toàn diện về React 19 Actions trong thực tế: useActionState, useOptimistic và khả năng phục hồi của Server Action với kiến trúc cấp độ sản phẩm và ví dụ mã.
Read more